diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts index 249284ea8..8741a2df2 100644 --- a/apps/api/src/app/symbol/symbol.controller.ts +++ b/apps/api/src/app/symbol/symbol.controller.ts @@ -1,6 +1,7 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; +import { ImportResponse } from '@ghostfolio/common/interfaces'; import { Controller, Get, @@ -75,6 +76,27 @@ export class SymbolController { return result; } + @Get(':dataSource/:symbol/dividends') + @UseGuards(AuthGuard('jwt')) + public async gatherDividends( + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string + ): Promise { + const result = await this.symbolService.getDividends({ + dataSource, + symbol + }); + + if (!result || isEmpty(result)) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + return result; + } + @Get(':dataSource/:symbol/:dateString') @UseGuards(AuthGuard('jwt')) public async gatherSymbolForDate( diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index 7a5f5586d..905590930 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -1,13 +1,17 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; +import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; import { IDataGatheringItem, IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; -import { DATE_FORMAT } from '@ghostfolio/common/helper'; -import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; +import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; +import { + HistoricalDataItem, + ImportResponse +} from '@ghostfolio/common/interfaces'; import { Injectable, Logger } from '@nestjs/common'; -import { format, subDays } from 'date-fns'; +import { format, subDays, subYears } from 'date-fns'; import { LookupItem } from './interfaces/lookup-item.interface'; import { SymbolItem } from './interfaces/symbol-item.interface'; @@ -16,7 +20,8 @@ import { SymbolItem } from './interfaces/symbol-item.interface'; export class SymbolService { public constructor( private readonly dataProviderService: DataProviderService, - private readonly marketDataService: MarketDataService + private readonly marketDataService: MarketDataService, + private readonly yahooFinanceService: YahooFinanceService ) {} public async get({ @@ -62,6 +67,47 @@ export class SymbolService { return undefined; } + public async getDividends({ + dataSource, + symbol + }: IDataGatheringItem): Promise { + const date = new Date(); + + // TODO: Use DataProviderService + const historicalData = await this.yahooFinanceService.getDividends( + symbol, + 'day', + subYears(date, 5), + date + ); + + return { + activities: Object.entries(historicalData[symbol]).map( + ([dateString, historicalDataItem]) => { + return { + accountId: undefined, + accountUserId: undefined, + comment: undefined, + createdAt: undefined, + date: parseDate(dateString), + fee: 0, + feeInBaseCurrency: 0, + id: undefined, + isDraft: false, + quantity: 0, + symbolProfileId: undefined, + type: 'DIVIDEND', + unitPrice: historicalDataItem.marketPrice, + updatedAt: undefined, + userId: undefined, + value: 0, + valueInBaseCurrency: 0 + }; + } + ) + }; + } + public async getForDate({ dataSource, date = new Date(), diff --git a/apps/api/src/services/data-provider/data-provider.module.ts b/apps/api/src/services/data-provider/data-provider.module.ts index 3fa56e06c..07982e698 100644 --- a/apps/api/src/services/data-provider/data-provider.module.ts +++ b/apps/api/src/services/data-provider/data-provider.module.ts @@ -59,6 +59,10 @@ import { DataProviderService } from './data-provider.service'; ] } ], - exports: [DataProviderService, GhostfolioScraperApiService] + exports: [ + DataProviderService, + GhostfolioScraperApiService, + YahooFinanceService + ] }) export class DataProviderModule {} 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 4e62b251e..25026ee01 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 @@ -160,6 +160,72 @@ export class YahooFinanceService implements DataProviderInterface { return response; } + public async getDividends( + aSymbol: string, + aGranularity: Granularity = 'day', + from: Date, + to: Date + ): Promise<{ + [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; + }> { + // TODO: Call getHistorical() + + if (isSameDay(from, to)) { + to = addDays(to, 1); + } + + const yahooFinanceSymbol = this.convertToYahooFinanceSymbol(aSymbol); + + try { + const historicalResult = await yahooFinance.historical( + yahooFinanceSymbol, + { + events: 'dividends', + interval: '1d', + period1: format(from, DATE_FORMAT), + period2: format(to, DATE_FORMAT) + } + ); + + const response: { + [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; + } = {}; + + // Convert symbol back + const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol); + + response[symbol] = {}; + + for (const historicalItem of historicalResult) { + let marketPrice = historicalItem.dividends; + + if (symbol === `${this.baseCurrency}GBp`) { + // Convert GPB to GBp (pence) + marketPrice = new Big(marketPrice).mul(100).toNumber(); + } else if (symbol === `${this.baseCurrency}ILA`) { + // Convert ILS to ILA + marketPrice = new Big(marketPrice).mul(100).toNumber(); + } else if (symbol === `${this.baseCurrency}ZAc`) { + // Convert ZAR to ZAc (cents) + marketPrice = new Big(marketPrice).mul(100).toNumber(); + } + + response[symbol][format(historicalItem.date, DATE_FORMAT)] = { + marketPrice + }; + } + + return response; + } catch (error) { + throw new Error( + `Could not get historical market data for ${aSymbol} (${this.getName()}) from ${format( + from, + DATE_FORMAT + )} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}` + ); + } + } + public async getHistorical( aSymbol: string, aGranularity: Granularity = 'day', diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index ef0a21d4e..cbd9f5905 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -11,7 +11,7 @@ import { IcsService } from '@ghostfolio/client/services/ics/ics.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { downloadAsFile } from '@ghostfolio/common/helper'; -import { User } from '@ghostfolio/common/interfaces'; +import { UniqueAsset, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DataSource, Order as OrderModel } from '@prisma/client'; import { format, parseISO } from 'date-fns'; @@ -198,6 +198,15 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit { }); } + public onImportDividends({ dataSource, symbol }: UniqueAsset) { + this.dataService + .fetchDividendsImport({ dataSource, symbol }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ activities }) => { + console.log(activities); + }); + } + public onUpdateActivity(aActivity: OrderModel) { this.router.navigate([], { queryParams: { activityId: aActivity.id, editDialog: true } diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.html b/apps/client/src/app/pages/portfolio/activities/activities-page.html index b810d777e..40e35caae 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.html +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -17,6 +17,7 @@ (export)="onExport($event)" (exportDrafts)="onExportDrafts($event)" (import)="onImport()" + (importDividends)="onImportDividends($event)" > diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 70c15d398..b6cfbc29a 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -24,6 +24,7 @@ import { BenchmarkResponse, Export, Filter, + ImportResponse, InfoItem, OAuthResponse, PortfolioDetails, @@ -119,6 +120,12 @@ export class DataService { }); } + public fetchDividendsImport({ dataSource, symbol }: UniqueAsset) { + return this.http.get( + `/api/v1/symbol/${dataSource}/${symbol}/dividends` + ); + } + public fetchExchangeRateForDate({ date, symbol diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index 462b412ba..682d19057 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -437,6 +437,18 @@ Export Draft as ICS +