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. 58
      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
Discover a variety of community projects for Ghostfolio: https://github.com/topics/ghostfolio

58
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<PortfolioPublicDetails> {
): Promise<PortfolioPublicResponse> {
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')

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

@ -41,6 +41,15 @@
>{{ baseUrl }}/{{ defaultLanguageCode }}/p/{{ element.id }}</a
>
</div>
@if (user?.settings?.isExperimentalFeatures) {
<div>
<code
>GET {{ baseUrl }}/api/v1/portfolio/public/{{
element.id
}}</code
>
</div>
}
}
</td>
</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 { 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<string>();

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

@ -10,6 +10,7 @@
<gf-access-table
[accesses]="accesses"
[showActions]="hasPermissionToDeleteAccess"
[user]="user"
(accessDeleted)="onDeleteAccess($event)"
/>
@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 {
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<PortfolioPosition, 'currency' | 'name'> & {
value: number;

4
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<PortfolioPublicDetails>(`/api/v1/portfolio/public/${aId}`)
.get<PortfolioPublicResponse>(`/api/v1/portfolio/public/${aId}`)
.pipe(
map((response) => {
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 { 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,

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;
hasDetails: boolean;
holdings: {
@ -22,3 +22,17 @@ export interface PortfolioPublicDetails {
>;
};
}
export interface PortfolioPublicResponseV1 {
performance: {
'1d': {
relativeChange: number;
};
max: {
relativeChange: number;
};
ytd: {
relativeChange: number;
};
};
}
Loading…
Cancel
Save