Browse Source

Extend Public API with portfolio performance metrics endpoint

pull/3762/head
Thomas Kaul 12 months ago
parent
commit
71c8a325e1
  1. 30
      README.md
  2. 48
      apps/api/src/app/portfolio/portfolio.controller.ts
  3. 9
      apps/client/src/app/components/access-table/access-table.component.html
  4. 3
      apps/client/src/app/components/access-table/access-table.component.ts
  5. 1
      apps/client/src/app/components/user-account-access/user-account-access.html
  6. 6
      apps/client/src/app/pages/public/public-page.component.ts
  7. 4
      apps/client/src/app/services/data.service.ts
  8. 4
      libs/common/src/lib/interfaces/index.ts
  9. 18
      libs/common/src/lib/interfaces/responses/portfolio-public-response.interface.ts

30
README.md

@ -220,6 +220,36 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TO
} }
``` ```
### Portfolio (experimental)
#### Prerequisites
Grant access of type _Public_ in the _Access_ tab of _My Ghostfolio_.
#### Request
`GET http://localhost:3333/api/v1/portfolio/public/<INSERT_ACCESS_ID>`
#### 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 ## Community Projects
Discover a variety of community projects for Ghostfolio: https://github.com/topics/ghostfolio Discover a variety of community projects for Ghostfolio: https://github.com/topics/ghostfolio

48
apps/api/src/app/portfolio/portfolio.controller.ts

@ -26,7 +26,7 @@ import {
PortfolioHoldingsResponse, PortfolioHoldingsResponse,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPublicDetails, PortfolioPublicResponse,
PortfolioReport PortfolioReport
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { import {
@ -501,7 +501,7 @@ export class PortfolioController {
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getPublic( public async getPublic(
@Param('accessId') accessId @Param('accessId') accessId
): Promise<PortfolioPublicDetails> { ): Promise<PortfolioPublicResponse> {
const access = await this.accessService.access({ id: accessId }); const access = await this.accessService.access({ id: accessId });
if (!access) { if (!access) {
@ -521,31 +521,59 @@ export class PortfolioController {
hasDetails = user.subscription.type === 'Premium'; hasDetails = user.subscription.type === 'Premium';
} }
const { holdings } = await this.portfolioService.getDetails({ const [
{ holdings },
{ performance: performance1d },
{ performance: performanceMax },
{ performance: performanceYtd }
] = await Promise.all([
this.portfolioService.getDetails({
filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }], filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
impersonationId: access.userId, impersonationId: access.userId,
userId: user.id, userId: user.id,
withMarkets: true withMarkets: true
}),
...['1d', 'max', 'ytd'].map((dateRange) => {
return this.portfolioService.getPerformance({
dateRange,
impersonationId: undefined,
userId: user.id
}); });
})
]);
const portfolioPublicDetails: PortfolioPublicDetails = { const portfolioPublicResponse: PortfolioPublicResponse = {
hasDetails, hasDetails,
alias: access.alias, alias: access.alias,
holdings: {} holdings: {},
performance: {
'1d': {
relativeChange:
performance1d.netPerformancePercentageWithCurrencyEffect
},
max: {
relativeChange:
performanceMax.netPerformancePercentageWithCurrencyEffect
},
ytd: {
relativeChange:
performanceYtd.netPerformancePercentageWithCurrencyEffect
}
}
}; };
const totalValue = Object.values(holdings) const totalValue = Object.values(holdings)
.map((portfolioPosition) => { .map(({ currency, marketPrice, quantity }) => {
return this.exchangeRateDataService.toCurrency( return this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice, quantity * marketPrice,
portfolioPosition.currency, currency,
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY
); );
}) })
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
for (const [symbol, portfolioPosition] of Object.entries(holdings)) { for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
portfolioPublicDetails.holdings[symbol] = { portfolioPublicResponse.holdings[symbol] = {
allocationInPercentage: allocationInPercentage:
portfolioPosition.valueInBaseCurrency / totalValue, portfolioPosition.valueInBaseCurrency / totalValue,
countries: hasDetails ? portfolioPosition.countries : [], countries: hasDetails ? portfolioPosition.countries : [],
@ -563,7 +591,7 @@ export class PortfolioController {
}; };
} }
return portfolioPublicDetails; return portfolioPublicResponse;
} }
@Get('position/:dataSource/:symbol') @Get('position/:dataSource/:symbol')

9
apps/client/src/app/components/access-table/access-table.component.html

@ -41,6 +41,15 @@
>{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a >{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a
> >
</div> </div>
@if (user?.settings?.isExperimentalFeatures) {
<div>
<code
>GET {{ baseUrl }}/api/v1/portfolio/public/{{
element.id
}}</code
>
</div>
}
} }
</td> </td>
</ng-container> </ng-container>

3
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 { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type';
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service'; import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { Access } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
@ -23,6 +23,7 @@ import { MatTableDataSource } from '@angular/material/table';
export class AccessTableComponent implements OnChanges, OnInit { export class AccessTableComponent implements OnChanges, OnInit {
@Input() accesses: Access[]; @Input() accesses: Access[];
@Input() showActions: boolean; @Input() showActions: boolean;
@Input() user: User;
@Output() accessDeleted = new EventEmitter<string>(); @Output() accessDeleted = new EventEmitter<string>();

1
apps/client/src/app/components/user-account-access/user-account-access.html

@ -10,6 +10,7 @@
<gf-access-table <gf-access-table
[accesses]="accesses" [accesses]="accesses"
[showActions]="hasPermissionToDeleteAccess" [showActions]="hasPermissionToDeleteAccess"
[user]="user"
(accessDeleted)="onDeleteAccess($event)" (accessDeleted)="onDeleteAccess($event)"
/> />
@if (hasPermissionToCreateAccess) { @if (hasPermissionToCreateAccess) {

6
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 { prettifySymbol } from '@ghostfolio/common/helper';
import { import {
PortfolioPosition, PortfolioPosition,
PortfolioPublicDetails PortfolioPublicResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Market } from '@ghostfolio/common/types'; import { Market } from '@ghostfolio/common/types';
@ -29,11 +29,11 @@ export class PublicPageComponent implements OnInit {
[code: string]: { name: string; value: number }; [code: string]: { name: string; value: number };
}; };
public deviceType: string; public deviceType: string;
public holdings: PortfolioPublicDetails['holdings'][string][]; public holdings: PortfolioPublicResponse['holdings'][string][];
public markets: { public markets: {
[key in Market]: { name: string; value: number }; [key in Market]: { name: string; value: number };
}; };
public portfolioPublicDetails: PortfolioPublicDetails; public portfolioPublicDetails: PortfolioPublicResponse;
public positions: { public positions: {
[symbol: string]: Pick<PortfolioPosition, 'currency' | 'name'> & { [symbol: string]: Pick<PortfolioPosition, 'currency' | 'name'> & {
value: number; value: number;

4
apps/client/src/app/services/data.service.ts

@ -36,7 +36,7 @@ import {
PortfolioHoldingsResponse, PortfolioHoldingsResponse,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPublicDetails, PortfolioPublicResponse,
PortfolioReport, PortfolioReport,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -611,7 +611,7 @@ export class DataService {
public fetchPortfolioPublic(aId: string) { public fetchPortfolioPublic(aId: string) {
return this.http return this.http
.get<PortfolioPublicDetails>(`/api/v1/portfolio/public/${aId}`) .get<PortfolioPublicResponse>(`/api/v1/portfolio/public/${aId}`)
.pipe( .pipe(
map((response) => { map((response) => {
if (response.holdings) { if (response.holdings) {

4
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 { PortfolioOverview } from './portfolio-overview.interface';
import type { PortfolioPerformance } from './portfolio-performance.interface'; import type { PortfolioPerformance } from './portfolio-performance.interface';
import type { PortfolioPosition } from './portfolio-position.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 { PortfolioReportRule } from './portfolio-report-rule.interface';
import type { PortfolioReport } from './portfolio-report.interface'; import type { PortfolioReport } from './portfolio-report.interface';
import type { PortfolioSummary } from './portfolio-summary.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 { OAuthResponse } from './responses/oauth-response.interface';
import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface'; import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface';
import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-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 { ScraperConfiguration } from './scraper-configuration.interface';
import type { Statistics } from './statistics.interface'; import type { Statistics } from './statistics.interface';
import type { Subscription } from './subscription.interface'; import type { Subscription } from './subscription.interface';
@ -91,7 +91,7 @@ export {
PortfolioPerformance, PortfolioPerformance,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPosition, PortfolioPosition,
PortfolioPublicDetails, PortfolioPublicResponse,
PortfolioReport, PortfolioReport,
PortfolioReportRule, PortfolioReportRule,
PortfolioSummary, PortfolioSummary,

18
libs/common/src/lib/interfaces/portfolio-public-details.interface.ts → 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; alias?: string;
hasDetails: boolean; hasDetails: boolean;
holdings: { holdings: {
@ -22,3 +22,17 @@ export interface PortfolioPublicDetails {
>; >;
}; };
} }
export interface PortfolioPublicResponseV1 {
performance: {
'1d': {
relativeChange: number;
};
max: {
relativeChange: number;
};
ytd: {
relativeChange: number;
};
};
}
Loading…
Cancel
Save