From b898c0678dedbc1c9242ab66ed6ee14bcefcd76a Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Sat, 14 Aug 2021 11:06:21 +0200 Subject: [PATCH] Feature/refactor exchange rate service (#289) * Refactor exchange rate service * Update changelog --- CHANGELOG.md | 6 +++ .../interfaces/lookup-item.interface.ts | 3 +- apps/api/src/app/symbol/symbol.service.ts | 1 + .../api/src/services/data-provider.service.ts | 14 +++++- .../ghostfolio-scraper-api.service.ts | 3 +- .../rakuten-rapid-api.service.ts | 3 +- .../yahoo-finance/yahoo-finance.service.ts | 8 +-- .../services/exchange-rate-data.service.ts | 50 +++++++++++-------- libs/common/src/lib/config.ts | 22 ++++++-- 9 files changed, 74 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52cf1010c..1c12f93d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/apps/api/src/app/symbol/interfaces/lookup-item.interface.ts b/apps/api/src/app/symbol/interfaces/lookup-item.interface.ts index e99cb893c..92bbf7662 100644 --- a/apps/api/src/app/symbol/interfaces/lookup-item.interface.ts +++ b/apps/api/src/app/symbol/interfaces/lookup-item.interface.ts @@ -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; diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index a44f4edd1..0db0ba066 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -39,6 +39,7 @@ export class SymbolService { const ghostfolioSymbolProfiles = await this.prismaService.symbolProfile.findMany({ select: { + currency: true, dataSource: true, name: true, symbol: true diff --git a/apps/api/src/services/data-provider.service.ts b/apps/api/src/services/data-provider.service.ts index 56a3cb708..64f82bd40 100644 --- a/apps/api/src/services/data-provider.service.ts +++ b/apps/api/src/services/data-provider.service.ts @@ -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( 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) { diff --git a/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts b/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts index e2ffe3d19..7b41cd12e 100644 --- a/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts +++ b/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts @@ -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: [] }; } diff --git a/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts b/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts index 944ada814..ee9f29ce7 100644 --- a/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts +++ b/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts @@ -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: [] }; } diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts index 93e601caf..3d0b8e7e5 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts @@ -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) diff --git a/apps/api/src/services/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data.service.ts index 6f2b71196..cc3fc4cb1 100644 --- a/apps/api/src/services/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data.service.ts @@ -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}`); } } diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 80269de77..6287929f2 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -8,11 +8,23 @@ export const benchmarks: Partial[] = [ { dataSource: DataSource.YAHOO, symbol: 'VOO' } ]; -export const currencyPairs: Partial[] = [ - { 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`;