From 583c14128bfab4e2a3b91fc40fbe03f1ccc84c3e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 21 Sep 2024 10:42:43 +0200 Subject: [PATCH] Feature/extend public api with portfolio performance metrics endpoint (#3762) * Extend Public API with portfolio performance metrics endpoint * Update changelog --- CHANGELOG.md | 1 + README.md | 30 ++++ apps/api/src/app/app.module.ts | 2 + .../app/endpoints/public/public.controller.ts | 134 ++++++++++++++++++ .../src/app/endpoints/public/public.module.ts | 49 +++++++ .../src/app/portfolio/portfolio.controller.ts | 81 +---------- .../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 | 30 ++-- .../src/app/pages/public/public-page.html | 10 +- apps/client/src/app/services/data.service.ts | 14 +- libs/common/src/lib/interfaces/index.ts | 4 +- .../public-portfolio-response.interface.ts} | 18 ++- 14 files changed, 275 insertions(+), 111 deletions(-) create mode 100644 apps/api/src/app/endpoints/public/public.controller.ts create mode 100644 apps/api/src/app/endpoints/public/public.module.ts rename libs/common/src/lib/interfaces/{portfolio-public-details.interface.ts => responses/public-portfolio-response.interface.ts} (55%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f419b6ba9..6c4ea5c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Extended the _Public API_ with a new endpoint that provides portfolio performance metrics (experimental) - Added a blog post: _Hacktoberfest 2024_ ### Changed diff --git a/README.md b/README.md index 365a473ee..0f4772d94 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,36 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous//portfolio` + +#### Response + +##### Success + +``` +{ + "performance": { + "1d": { + "relativeChange": 0 // normalized from -1 to 1 + }; + "ytd": { + "relativeChange": 0 // normalized from -1 to 1 + }, + "max": { + "relativeChange": 0 // normalized from -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/app.module.ts b/apps/api/src/app/app.module.ts index 86d97eaf8..2803a0580 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -31,6 +31,7 @@ import { AuthDeviceModule } from './auth-device/auth-device.module'; import { AuthModule } from './auth/auth.module'; import { BenchmarkModule } from './benchmark/benchmark.module'; import { CacheModule } from './cache/cache.module'; +import { PublicModule } from './endpoints/public/public.module'; import { ExchangeRateModule } from './exchange-rate/exchange-rate.module'; import { ExportModule } from './export/export.module'; import { HealthModule } from './health/health.module'; @@ -85,6 +86,7 @@ import { UserModule } from './user/user.module'; PortfolioSnapshotQueueModule, PrismaModule, PropertyModule, + PublicModule, RedisCacheModule, ScheduleModule.forRoot(), ServeStaticModule.forRoot({ diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts new file mode 100644 index 000000000..4e931372f --- /dev/null +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -0,0 +1,134 @@ +import { AccessService } from '@ghostfolio/api/app/access/access.service'; +import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; +import { UserService } from '@ghostfolio/api/app/user/user.service'; +import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; +import { getSum } from '@ghostfolio/common/helper'; +import { PublicPortfolioResponse } from '@ghostfolio/common/interfaces'; +import type { RequestWithUser } from '@ghostfolio/common/types'; + +import { + Controller, + Get, + HttpException, + Inject, + Param, + UseInterceptors +} from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; +import { Big } from 'big.js'; +import { StatusCodes, getReasonPhrase } from 'http-status-codes'; + +@Controller('public') +export class PublicController { + public constructor( + private readonly accessService: AccessService, + private readonly configurationService: ConfigurationService, + private readonly exchangeRateDataService: ExchangeRateDataService, + private readonly portfolioService: PortfolioService, + @Inject(REQUEST) private readonly request: RequestWithUser, + private readonly userService: UserService + ) {} + + @Get(':accessId/portfolio') + @UseInterceptors(TransformDataSourceInResponseInterceptor) + public async getPublicPortfolio( + @Param('accessId') accessId + ): Promise { + const access = await this.accessService.access({ id: accessId }); + + if (!access) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + let hasDetails = true; + + const user = await this.userService.user({ + id: access.userId + }); + + if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { + hasDetails = user.subscription.type === 'Premium'; + } + + 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 publicPortfolioResponse: PublicPortfolioResponse = { + hasDetails, + alias: access.alias, + holdings: {}, + performance: { + '1d': { + relativeChange: + performance1d.netPerformancePercentageWithCurrencyEffect + }, + max: { + relativeChange: + performanceMax.netPerformancePercentageWithCurrencyEffect + }, + ytd: { + relativeChange: + performanceYtd.netPerformancePercentageWithCurrencyEffect + } + } + }; + + const totalValue = getSum( + Object.values(holdings).map(({ currency, marketPrice, quantity }) => { + return new Big( + this.exchangeRateDataService.toCurrency( + quantity * marketPrice, + currency, + this.request.user?.Settings?.settings.baseCurrency ?? + DEFAULT_CURRENCY + ) + ); + }) + ).toNumber(); + + for (const [symbol, portfolioPosition] of Object.entries(holdings)) { + publicPortfolioResponse.holdings[symbol] = { + allocationInPercentage: + portfolioPosition.valueInBaseCurrency / totalValue, + countries: hasDetails ? portfolioPosition.countries : [], + currency: hasDetails ? portfolioPosition.currency : undefined, + dataSource: portfolioPosition.dataSource, + dateOfFirstActivity: portfolioPosition.dateOfFirstActivity, + markets: hasDetails ? portfolioPosition.markets : undefined, + name: portfolioPosition.name, + netPerformancePercentWithCurrencyEffect: + portfolioPosition.netPerformancePercentWithCurrencyEffect, + sectors: hasDetails ? portfolioPosition.sectors : [], + symbol: portfolioPosition.symbol, + url: portfolioPosition.url, + valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue + }; + } + + return publicPortfolioResponse; + } +} diff --git a/apps/api/src/app/endpoints/public/public.module.ts b/apps/api/src/app/endpoints/public/public.module.ts new file mode 100644 index 000000000..9b43522c1 --- /dev/null +++ b/apps/api/src/app/endpoints/public/public.module.ts @@ -0,0 +1,49 @@ +import { AccessModule } from '@ghostfolio/api/app/access/access.module'; +import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; +import { AccountService } from '@ghostfolio/api/app/account/account.service'; +import { OrderModule } from '@ghostfolio/api/app/order/order.module'; +import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; +import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service'; +import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; +import { UserModule } from '@ghostfolio/api/app/user/user.module'; +import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; +import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; +import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; +import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; +import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; +import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; +import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module'; +import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; + +import { Module } from '@nestjs/common'; + +import { PublicController } from './public.controller'; + +@Module({ + controllers: [PublicController], + imports: [ + AccessModule, + DataProviderModule, + ExchangeRateDataModule, + ImpersonationModule, + MarketDataModule, + OrderModule, + PortfolioSnapshotQueueModule, + PrismaModule, + RedisCacheModule, + SymbolProfileModule, + TransformDataSourceInRequestModule, + UserModule + ], + providers: [ + AccountBalanceService, + AccountService, + CurrentRateService, + PortfolioCalculatorFactory, + PortfolioService, + RulesService + ] +}) +export class PublicModule {} diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 036e48901..9f5635cf5 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -1,6 +1,5 @@ import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service'; -import { UserService } from '@ghostfolio/api/app/user/user.service'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { @@ -13,20 +12,15 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; -import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; -import { - DEFAULT_CURRENCY, - HEADER_KEY_IMPERSONATION -} from '@ghostfolio/common/config'; +import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { PortfolioDetails, PortfolioDividends, PortfolioHoldingsResponse, PortfolioInvestments, PortfolioPerformanceResponse, - PortfolioPublicDetails, PortfolioReport } from '@ghostfolio/common/interfaces'; import { @@ -70,12 +64,10 @@ export class PortfolioController { private readonly accessService: AccessService, private readonly apiService: ApiService, private readonly configurationService: ConfigurationService, - private readonly exchangeRateDataService: ExchangeRateDataService, private readonly impersonationService: ImpersonationService, private readonly orderService: OrderService, private readonly portfolioService: PortfolioService, - @Inject(REQUEST) private readonly request: RequestWithUser, - private readonly userService: UserService + @Inject(REQUEST) private readonly request: RequestWithUser ) {} @Get('details') @@ -497,75 +489,6 @@ export class PortfolioController { return performanceInformation; } - @Get('public/:accessId') - @UseInterceptors(TransformDataSourceInResponseInterceptor) - public async getPublic( - @Param('accessId') accessId - ): Promise { - const access = await this.accessService.access({ id: accessId }); - - if (!access) { - throw new HttpException( - getReasonPhrase(StatusCodes.NOT_FOUND), - StatusCodes.NOT_FOUND - ); - } - - let hasDetails = true; - - const user = await this.userService.user({ - id: access.userId - }); - - if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { - 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 portfolioPublicDetails: PortfolioPublicDetails = { - hasDetails, - alias: access.alias, - holdings: {} - }; - - const totalValue = Object.values(holdings) - .map((portfolioPosition) => { - return this.exchangeRateDataService.toCurrency( - portfolioPosition.quantity * portfolioPosition.marketPrice, - portfolioPosition.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] = { - allocationInPercentage: - portfolioPosition.valueInBaseCurrency / totalValue, - countries: hasDetails ? portfolioPosition.countries : [], - currency: hasDetails ? portfolioPosition.currency : undefined, - dataSource: portfolioPosition.dataSource, - dateOfFirstActivity: portfolioPosition.dateOfFirstActivity, - markets: hasDetails ? portfolioPosition.markets : undefined, - name: portfolioPosition.name, - netPerformancePercentWithCurrencyEffect: - portfolioPosition.netPerformancePercentWithCurrencyEffect, - sectors: hasDetails ? portfolioPosition.sectors : [], - symbol: portfolioPosition.symbol, - url: portfolioPosition.url, - valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue - }; - } - - return portfolioPublicDetails; - } - @Get('position/:dataSource/:symbol') @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor) 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 e625cbf75..8716873a7 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 @@ -39,6 +39,15 @@ getPublicUrl(element.id) }} + @if (user?.settings?.isExperimentalFeatures) { +
+ GET {{ baseUrl }}/api/v1/public/{{ + element.id + }}/portfolio +
+ } } 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 3d47c6087..94756634b 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 { Clipboard } from '@angular/cdk/clipboard'; import { @@ -24,6 +24,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..5e901c3f5 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 + PublicPortfolioResponse } from '@ghostfolio/common/interfaces'; import { Market } from '@ghostfolio/common/types'; @@ -29,16 +29,16 @@ export class PublicPageComponent implements OnInit { [code: string]: { name: string; value: number }; }; public deviceType: string; - public holdings: PortfolioPublicDetails['holdings'][string][]; + public holdings: PublicPortfolioResponse['holdings'][string][]; public markets: { [key in Market]: { name: string; value: number }; }; - public portfolioPublicDetails: PortfolioPublicDetails; public positions: { [symbol: string]: Pick & { value: number; }; }; + public publicPortfolioDetails: PublicPortfolioResponse; public sectors: { [name: string]: { name: string; value: number }; }; @@ -47,7 +47,7 @@ export class PublicPageComponent implements OnInit { }; public UNKNOWN_KEY = UNKNOWN_KEY; - private id: string; + private accessId: string; private unsubscribeSubject = new Subject(); public constructor( @@ -58,7 +58,7 @@ export class PublicPageComponent implements OnInit { private router: Router ) { this.activatedRoute.params.subscribe((params) => { - this.id = params['id']; + this.accessId = params['id']; }); } @@ -66,7 +66,7 @@ export class PublicPageComponent implements OnInit { this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.dataService - .fetchPortfolioPublic(this.id) + .fetchPublicPortfolio(this.accessId) .pipe( takeUntil(this.unsubscribeSubject), catchError((error) => { @@ -79,7 +79,7 @@ export class PublicPageComponent implements OnInit { }) ) .subscribe((portfolioPublicDetails) => { - this.portfolioPublicDetails = portfolioPublicDetails; + this.publicPortfolioDetails = portfolioPublicDetails; this.initializeAnalysisData(); @@ -135,7 +135,7 @@ export class PublicPageComponent implements OnInit { }; for (const [symbol, position] of Object.entries( - this.portfolioPublicDetails.holdings + this.publicPortfolioDetails.holdings )) { this.holdings.push(position); @@ -164,7 +164,7 @@ export class PublicPageComponent implements OnInit { name: continent, value: weight * - this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency + this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency }; } @@ -175,19 +175,19 @@ export class PublicPageComponent implements OnInit { name, value: weight * - this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency + this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency }; } } } else { this.continents[UNKNOWN_KEY].value += - this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency; + this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency; this.countries[UNKNOWN_KEY].value += - this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency; + this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency; this.markets[UNKNOWN_KEY].value += - this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency; + this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency; } if (position.sectors.length > 0) { @@ -201,13 +201,13 @@ export class PublicPageComponent implements OnInit { name, value: weight * - this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency + this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency }; } } } else { this.sectors[UNKNOWN_KEY].value += - this.portfolioPublicDetails.holdings[symbol].valueInBaseCurrency; + this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency; } this.symbols[prettifySymbol(symbol)] = { diff --git a/apps/client/src/app/pages/public/public-page.html b/apps/client/src/app/pages/public/public-page.html index 04d8aca1e..fe213166f 100644 --- a/apps/client/src/app/pages/public/public-page.html +++ b/apps/client/src/app/pages/public/public-page.html @@ -2,7 +2,7 @@

- Hello, {{ portfolioPublicDetails?.alias ?? 'someone' }} has shared a + Hello, {{ publicPortfolioDetails?.alias ?? 'someone' }} has shared a Portfolio with you!

@@ -24,7 +24,7 @@
- @if (portfolioPublicDetails?.hasDetails) { + @if (publicPortfolioDetails?.hasDetails) {
@@ -43,7 +43,7 @@
} - @if (portfolioPublicDetails?.hasDetails) { + @if (publicPortfolioDetails?.hasDetails) {
@@ -60,7 +60,7 @@
} - @if (portfolioPublicDetails?.hasDetails) { + @if (publicPortfolioDetails?.hasDetails) {
@@ -79,7 +79,7 @@
} - @if (portfolioPublicDetails?.hasDetails) { + @if (publicPortfolioDetails?.hasDetails) {
diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 78373adcc..4135335c6 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -36,8 +36,8 @@ import { PortfolioHoldingsResponse, PortfolioInvestments, PortfolioPerformanceResponse, - PortfolioPublicDetails, PortfolioReport, + PublicPortfolioResponse, User } from '@ghostfolio/common/interfaces'; import { filterGlobalPermissions } from '@ghostfolio/common/permissions'; @@ -611,9 +611,13 @@ export class DataService { ); } - public fetchPortfolioPublic(aId: string) { + public fetchPortfolioReport() { + return this.http.get('/api/v1/portfolio/report'); + } + + public fetchPublicPortfolio(aAccessId: string) { return this.http - .get(`/api/v1/portfolio/public/${aId}`) + .get(`/api/v1/public/${aAccessId}/portfolio`) .pipe( map((response) => { if (response.holdings) { @@ -631,10 +635,6 @@ export class DataService { ); } - public fetchPortfolioReport() { - return this.http.get('/api/v1/portfolio/report'); - } - public loginAnonymous(accessToken: string) { return this.http.post(`/api/v1/auth/anonymous`, { accessToken diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index efab780fd..51bb7c10e 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 { PublicPortfolioResponse } from './responses/public-portfolio-response.interface'; import type { ScraperConfiguration } from './scraper-configuration.interface'; import type { Statistics } from './statistics.interface'; import type { Subscription } from './subscription.interface'; @@ -91,12 +91,12 @@ export { PortfolioPerformance, PortfolioPerformanceResponse, PortfolioPosition, - PortfolioPublicDetails, PortfolioReport, PortfolioReportRule, PortfolioSummary, Position, Product, + PublicPortfolioResponse, ResponseError, ScraperConfiguration, Statistics, diff --git a/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts similarity index 55% rename from libs/common/src/lib/interfaces/portfolio-public-details.interface.ts rename to libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts index 57b0b36cc..f7ce78479 100644 --- a/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts +++ b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts @@ -1,6 +1,6 @@ -import { PortfolioPosition } from '@ghostfolio/common/interfaces'; +import { PortfolioPosition } from '../portfolio-position.interface'; -export interface PortfolioPublicDetails { +export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { alias?: string; hasDetails: boolean; holdings: { @@ -22,3 +22,17 @@ export interface PortfolioPublicDetails { >; }; } + +interface PublicPortfolioResponseV1 { + performance: { + '1d': { + relativeChange: number; + }; + max: { + relativeChange: number; + }; + ytd: { + relativeChange: number; + }; + }; +}