From 71c8a325e109c5ab2dea5fb31af0ebc7f373a48e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 13 Sep 2024 21:51:26 +0200 Subject: [PATCH] Extend Public API with portfolio performance metrics endpoint --- README.md | 30 ++++++++++ .../src/app/portfolio/portfolio.controller.ts | 58 ++++++++++++++----- .../access-table/access-table.component.html | 9 +++ .../access-table/access-table.component.ts | 3 +- .../user-account-access.html | 1 + .../app/pages/public/public-page.component.ts | 6 +- apps/client/src/app/services/data.service.ts | 4 +- libs/common/src/lib/interfaces/index.ts | 4 +- .../portfolio-public-response.interface.ts} | 18 +++++- 9 files changed, 108 insertions(+), 25 deletions(-) rename libs/common/src/lib/interfaces/{portfolio-public-details.interface.ts => responses/portfolio-public-response.interface.ts} (54%) diff --git a/README.md b/README.md index 365a473ee..a7043d715 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,36 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/` + +#### Response + +##### Success + +``` +{ + "performance": { + "1d": { + "relativeChange": 0 // normalized (-1 to 1) + }; + "ytd": { + "relativeChange": 0 // normalized (-1 to 1) + }, + "max": { + "relativeChange": 0 // normalized (-1 to 1) + } + } +} +``` + ## Community Projects Discover a variety of community projects for Ghostfolio: https://github.com/topics/ghostfolio diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 036e48901..1b0100f3e 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -26,7 +26,7 @@ import { PortfolioHoldingsResponse, PortfolioInvestments, PortfolioPerformanceResponse, - PortfolioPublicDetails, + PortfolioPublicResponse, PortfolioReport } from '@ghostfolio/common/interfaces'; import { @@ -501,7 +501,7 @@ export class PortfolioController { @UseInterceptors(TransformDataSourceInResponseInterceptor) public async getPublic( @Param('accessId') accessId - ): Promise { + ): Promise { const access = await this.accessService.access({ id: accessId }); if (!access) { @@ -521,31 +521,59 @@ export class PortfolioController { hasDetails = user.subscription.type === 'Premium'; } - const { holdings } = await this.portfolioService.getDetails({ - filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }], - impersonationId: access.userId, - userId: user.id, - withMarkets: true - }); + const [ + { holdings }, + { performance: performance1d }, + { performance: performanceMax }, + { performance: performanceYtd } + ] = await Promise.all([ + this.portfolioService.getDetails({ + filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }], + impersonationId: access.userId, + userId: user.id, + withMarkets: true + }), + ...['1d', 'max', 'ytd'].map((dateRange) => { + return this.portfolioService.getPerformance({ + dateRange, + impersonationId: undefined, + userId: user.id + }); + }) + ]); - const portfolioPublicDetails: PortfolioPublicDetails = { + const portfolioPublicResponse: PortfolioPublicResponse = { hasDetails, alias: access.alias, - holdings: {} + holdings: {}, + performance: { + '1d': { + relativeChange: + performance1d.netPerformancePercentageWithCurrencyEffect + }, + max: { + relativeChange: + performanceMax.netPerformancePercentageWithCurrencyEffect + }, + ytd: { + relativeChange: + performanceYtd.netPerformancePercentageWithCurrencyEffect + } + } }; const totalValue = Object.values(holdings) - .map((portfolioPosition) => { + .map(({ currency, marketPrice, quantity }) => { return this.exchangeRateDataService.toCurrency( - portfolioPosition.quantity * portfolioPosition.marketPrice, - portfolioPosition.currency, + quantity * marketPrice, + currency, this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY ); }) .reduce((a, b) => a + b, 0); for (const [symbol, portfolioPosition] of Object.entries(holdings)) { - portfolioPublicDetails.holdings[symbol] = { + portfolioPublicResponse.holdings[symbol] = { allocationInPercentage: portfolioPosition.valueInBaseCurrency / totalValue, countries: hasDetails ? portfolioPosition.countries : [], @@ -563,7 +591,7 @@ export class PortfolioController { }; } - return portfolioPublicDetails; + return portfolioPublicResponse; } @Get('position/:dataSource/:symbol') diff --git a/apps/client/src/app/components/access-table/access-table.component.html b/apps/client/src/app/components/access-table/access-table.component.html index b1befc8c9..e65bceeb8 100644 --- a/apps/client/src/app/components/access-table/access-table.component.html +++ b/apps/client/src/app/components/access-table/access-table.component.html @@ -41,6 +41,15 @@ >{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }} + @if (user?.settings?.isExperimentalFeatures) { +
+ GET {{ baseUrl }}/api/v1/portfolio/public/{{ + element.id + }} +
+ } } diff --git a/apps/client/src/app/components/access-table/access-table.component.ts b/apps/client/src/app/components/access-table/access-table.component.ts index 7772451d4..aa1007674 100644 --- a/apps/client/src/app/components/access-table/access-table.component.ts +++ b/apps/client/src/app/components/access-table/access-table.component.ts @@ -1,7 +1,7 @@ import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; -import { Access } from '@ghostfolio/common/interfaces'; +import { Access, User } from '@ghostfolio/common/interfaces'; import { ChangeDetectionStrategy, @@ -23,6 +23,7 @@ import { MatTableDataSource } from '@angular/material/table'; export class AccessTableComponent implements OnChanges, OnInit { @Input() accesses: Access[]; @Input() showActions: boolean; + @Input() user: User; @Output() accessDeleted = new EventEmitter(); diff --git a/apps/client/src/app/components/user-account-access/user-account-access.html b/apps/client/src/app/components/user-account-access/user-account-access.html index f651b0419..e5d43cadc 100644 --- a/apps/client/src/app/components/user-account-access/user-account-access.html +++ b/apps/client/src/app/components/user-account-access/user-account-access.html @@ -10,6 +10,7 @@ @if (hasPermissionToCreateAccess) { diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts index 4e593b959..2ab604cbe 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -3,7 +3,7 @@ import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { prettifySymbol } from '@ghostfolio/common/helper'; import { PortfolioPosition, - PortfolioPublicDetails + PortfolioPublicResponse } from '@ghostfolio/common/interfaces'; import { Market } from '@ghostfolio/common/types'; @@ -29,11 +29,11 @@ export class PublicPageComponent implements OnInit { [code: string]: { name: string; value: number }; }; public deviceType: string; - public holdings: PortfolioPublicDetails['holdings'][string][]; + public holdings: PortfolioPublicResponse['holdings'][string][]; public markets: { [key in Market]: { name: string; value: number }; }; - public portfolioPublicDetails: PortfolioPublicDetails; + public portfolioPublicDetails: PortfolioPublicResponse; public positions: { [symbol: string]: Pick & { value: number; diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index cbdde0265..5098d7fc2 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -36,7 +36,7 @@ import { PortfolioHoldingsResponse, PortfolioInvestments, PortfolioPerformanceResponse, - PortfolioPublicDetails, + PortfolioPublicResponse, PortfolioReport, User } from '@ghostfolio/common/interfaces'; @@ -611,7 +611,7 @@ export class DataService { public fetchPortfolioPublic(aId: string) { return this.http - .get(`/api/v1/portfolio/public/${aId}`) + .get(`/api/v1/portfolio/public/${aId}`) .pipe( map((response) => { if (response.holdings) { diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index efab780fd..1dde1ba30 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -31,7 +31,6 @@ import type { PortfolioItem } from './portfolio-item.interface'; import type { PortfolioOverview } from './portfolio-overview.interface'; import type { PortfolioPerformance } from './portfolio-performance.interface'; import type { PortfolioPosition } from './portfolio-position.interface'; -import type { PortfolioPublicDetails } from './portfolio-public-details.interface'; import type { PortfolioReportRule } from './portfolio-report-rule.interface'; import type { PortfolioReport } from './portfolio-report.interface'; import type { PortfolioSummary } from './portfolio-summary.interface'; @@ -44,6 +43,7 @@ import type { ImportResponse } from './responses/import-response.interface'; import type { OAuthResponse } from './responses/oauth-response.interface'; import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface'; import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface'; +import type { PortfolioPublicResponse } from './responses/portfolio-public-response.interface'; import type { ScraperConfiguration } from './scraper-configuration.interface'; import type { Statistics } from './statistics.interface'; import type { Subscription } from './subscription.interface'; @@ -91,7 +91,7 @@ export { PortfolioPerformance, PortfolioPerformanceResponse, PortfolioPosition, - PortfolioPublicDetails, + PortfolioPublicResponse, PortfolioReport, PortfolioReportRule, PortfolioSummary, diff --git a/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-public-response.interface.ts similarity index 54% rename from libs/common/src/lib/interfaces/portfolio-public-details.interface.ts rename to libs/common/src/lib/interfaces/responses/portfolio-public-response.interface.ts index 57b0b36cc..778fdd48e 100644 --- a/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts +++ b/libs/common/src/lib/interfaces/responses/portfolio-public-response.interface.ts @@ -1,6 +1,6 @@ -import { PortfolioPosition } from '@ghostfolio/common/interfaces'; +import { PortfolioPosition } from '../portfolio-position.interface'; -export interface PortfolioPublicDetails { +export interface PortfolioPublicResponse extends PortfolioPublicResponseV1 { alias?: string; hasDetails: boolean; holdings: { @@ -22,3 +22,17 @@ export interface PortfolioPublicDetails { >; }; } + +export interface PortfolioPublicResponseV1 { + performance: { + '1d': { + relativeChange: number; + }; + max: { + relativeChange: number; + }; + ytd: { + relativeChange: number; + }; + }; +}