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/),
|
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…
x
Reference in New Issue
Block a user