From a7d61177238190d24da0edcd32b7a0502f14f40c Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Dec 2022 16:47:09 +0100 Subject: [PATCH] Add exchange rate endpoint --- apps/api/src/app/app.module.ts | 2 + .../exchange-rate/exchange-rate.controller.ts | 26 ++++++++++ .../app/exchange-rate/exchange-rate.module.ts | 13 +++++ .../exchange-rate/exchange-rate.service.ts | 29 ++++++++++++ apps/api/src/app/symbol/symbol.service.ts | 9 +--- .../src/services/exchange-rate-data.module.ts | 6 ++- .../services/exchange-rate-data.service.ts | 47 ++++++++++++++++++- ...ate-or-update-activity-dialog.component.ts | 10 ++-- apps/client/src/app/services/data.service.ts | 20 +++++--- 9 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 apps/api/src/app/exchange-rate/exchange-rate.controller.ts create mode 100644 apps/api/src/app/exchange-rate/exchange-rate.module.ts create mode 100644 apps/api/src/app/exchange-rate/exchange-rate.service.ts diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index e41b60b0e..d22a53b51 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -20,6 +20,7 @@ import { AccountModule } from './account/account.module'; import { AdminModule } from './admin/admin.module'; import { AppController } from './app.controller'; import { AuthModule } from './auth/auth.module'; +import { ExchangeRateModule } from './exchange-rate/exchange-rate.module'; import { BenchmarkModule } from './benchmark/benchmark.module'; import { CacheModule } from './cache/cache.module'; import { ExportModule } from './export/export.module'; @@ -52,6 +53,7 @@ import { UserModule } from './user/user.module'; ConfigurationModule, DataGatheringModule, DataProviderModule, + ExchangeRateModule, ExchangeRateDataModule, ExportModule, ImportModule, diff --git a/apps/api/src/app/exchange-rate/exchange-rate.controller.ts b/apps/api/src/app/exchange-rate/exchange-rate.controller.ts new file mode 100644 index 000000000..75a6f57b4 --- /dev/null +++ b/apps/api/src/app/exchange-rate/exchange-rate.controller.ts @@ -0,0 +1,26 @@ +import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +import { ExchangeRateService } from './exchange-rate.service'; + +@Controller('exchange-rate') +export class ExchangeRateController { + public constructor( + private readonly exchangeRateService: ExchangeRateService + ) {} + + @Get(':symbol/:dateString') + @UseGuards(AuthGuard('jwt')) + public async getExchangeRate( + @Param('dateString') dateString: string, + @Param('symbol') symbol: string + ): Promise { + const date = new Date(dateString); + + return this.exchangeRateService.getExchangeRate({ + date, + symbol + }); + } +} diff --git a/apps/api/src/app/exchange-rate/exchange-rate.module.ts b/apps/api/src/app/exchange-rate/exchange-rate.module.ts new file mode 100644 index 000000000..04337d010 --- /dev/null +++ b/apps/api/src/app/exchange-rate/exchange-rate.module.ts @@ -0,0 +1,13 @@ +import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module'; +import { Module } from '@nestjs/common'; + +import { ExchangeRateController } from './exchange-rate.controller'; +import { ExchangeRateService } from './exchange-rate.service'; + +@Module({ + controllers: [ExchangeRateController], + exports: [ExchangeRateService], + imports: [ExchangeRateDataModule], + providers: [ExchangeRateService] +}) +export class ExchangeRateModule {} diff --git a/apps/api/src/app/exchange-rate/exchange-rate.service.ts b/apps/api/src/app/exchange-rate/exchange-rate.service.ts new file mode 100644 index 000000000..be7f3d55f --- /dev/null +++ b/apps/api/src/app/exchange-rate/exchange-rate.service.ts @@ -0,0 +1,29 @@ +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; +import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ExchangeRateService { + public constructor( + private readonly exchangeRateDataService: ExchangeRateDataService + ) {} + + public async getExchangeRate({ + date, + symbol + }: { + date: Date; + symbol: string; + }): Promise { + const [currency1, currency2] = symbol.split('-'); + + const marketPrice = await this.exchangeRateDataService.toCurrencyAtDate( + 1, + currency1, + currency2, + date + ); + + return { marketPrice }; + } +} diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index 0575cfda1..7a5f5586d 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -7,7 +7,6 @@ import { MarketDataService } from '@ghostfolio/api/services/market-data.service' import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; import { format, subDays } from 'date-fns'; import { LookupItem } from './interfaces/lookup-item.interface'; @@ -65,13 +64,9 @@ export class SymbolService { public async getForDate({ dataSource, - date, + date = new Date(), symbol - }: { - dataSource: DataSource; - date: Date; - symbol: string; - }): Promise { + }: IDataGatheringItem): Promise { const historicalData = await this.dataProviderService.getHistoricalRaw( [{ dataSource, symbol }], date, diff --git a/apps/api/src/services/exchange-rate-data.module.ts b/apps/api/src/services/exchange-rate-data.module.ts index 8b8eeee28..59484f2d6 100644 --- a/apps/api/src/services/exchange-rate-data.module.ts +++ b/apps/api/src/services/exchange-rate-data.module.ts @@ -3,17 +3,19 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data- import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { Module } from '@nestjs/common'; +import { MarketDataModule } from './market-data.module'; import { PrismaModule } from './prisma.module'; @Module({ + exports: [ExchangeRateDataService], imports: [ ConfigurationModule, DataProviderModule, + MarketDataModule, PrismaModule, PropertyModule ], - providers: [ExchangeRateDataService], - exports: [ExchangeRateDataService] + providers: [ExchangeRateDataService] }) export class ExchangeRateDataModule {} diff --git a/apps/api/src/services/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data.service.ts index 60a7e0e56..e68b4ef52 100644 --- a/apps/api/src/services/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data.service.ts @@ -1,12 +1,13 @@ import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config'; import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper'; import { Injectable, Logger } from '@nestjs/common'; -import { format } from 'date-fns'; +import { format, isToday } from 'date-fns'; import { isNumber, uniq } from 'lodash'; import { ConfigurationService } from './configuration.service'; import { DataProviderService } from './data-provider/data-provider.service'; import { IDataGatheringItem } from './interfaces/interfaces'; +import { MarketDataService } from './market-data.service'; import { PrismaService } from './prisma.service'; import { PropertyService } from './property/property.service'; @@ -20,6 +21,7 @@ export class ExchangeRateDataService { public constructor( private readonly configurationService: ConfigurationService, private readonly dataProviderService: DataProviderService, + private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, private readonly propertyService: PropertyService ) {} @@ -152,6 +154,49 @@ export class ExchangeRateDataService { return aValue; } + public async toCurrencyAtDate( + aValue: number, + aFromCurrency: string, + aToCurrency: string, + aDate: Date + ) { + if (aValue === 0) { + return 0; + } + + if (isToday(aDate)) { + return this.toCurrency(aValue, aFromCurrency, aToCurrency); + } + + let factor = 1; + + if (aFromCurrency !== aToCurrency) { + const marketData = await this.marketDataService.get({ + dataSource: this.dataProviderService.getPrimaryDataSource(), + date: aDate, + symbol: `${aFromCurrency}${aToCurrency}` + }); + + if (marketData?.marketPrice) { + factor = marketData?.marketPrice; + } else { + // TODO: Calculate indirectly via base currency + return this.toCurrency(aValue, aFromCurrency, aToCurrency); + } + } + + if (isNumber(factor) && !isNaN(factor)) { + return factor * aValue; + } + + // Fallback with error, if currencies are not available + Logger.error( + `No exchange rate has been found for ${aFromCurrency}${aToCurrency}`, + 'ExchangeRateDataService' + ); + return aValue; + } + private async prepareCurrencies(): Promise { let currencies: string[] = []; diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index ff4085d65..2f93589e5 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -13,7 +13,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; -import { AdminService } from '@ghostfolio/client/services/admin.service'; import { DataService } from '@ghostfolio/client/services/data.service'; import { translate } from '@ghostfolio/ui/i18n'; import { AssetClass, AssetSubClass, Type } from '@prisma/client'; @@ -60,7 +59,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { private unsubscribeSubject = new Subject(); public constructor( - private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateActivityDialogParams, private dataService: DataService, @@ -125,12 +123,10 @@ export class CreateOrUpdateActivityDialog implements OnDestroy { if (currency && currencyOfFee && currency !== currencyOfFee && date) { try { const { marketPrice } = await lastValueFrom( - // TODO: Create endpoint for exchange rate conversion - this.adminService - .fetchSymbolForDate({ + this.dataService + .fetchExchangeRateForDate({ date, - dataSource: 'YAHOO', - symbol: `${currencyOfFee}${currency}` + symbol: `${currencyOfFee}-${currency}` }) .pipe(takeUntil(this.unsubscribeSubject)) ); diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 6bb4b0dba..fc98da0fa 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -12,6 +12,7 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.in import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface'; import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface'; import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto'; +import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { PropertyDto } from '@ghostfolio/api/services/property/property.dto'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { @@ -36,12 +37,7 @@ import { import { filterGlobalPermissions } from '@ghostfolio/common/permissions'; import { AccountWithValue, DateRange } from '@ghostfolio/common/types'; import { translate } from '@ghostfolio/ui/i18n'; -import { - AssetClass, - AssetSubClass, - DataSource, - Order as OrderModel -} from '@prisma/client'; +import { DataSource, Order as OrderModel } from '@prisma/client'; import { format, parseISO } from 'date-fns'; import { cloneDeep, groupBy } from 'lodash'; import { Observable } from 'rxjs'; @@ -104,6 +100,18 @@ export class DataService { }); } + public fetchExchangeRateForDate({ + date, + symbol + }: { + date: Date; + symbol: string; + }) { + return this.http.get( + `/api/v1/exchange-rate/${symbol}/${format(date, DATE_FORMAT)}` + ); + } + public deleteAccess(aId: string) { return this.http.delete(`/api/v1/access/${aId}`); }