Browse Source

Feature/extend X-ray page by summary (#4107)

* Add summary to X-ray page

* Update changelog
pull/4111/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
291be3e605
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 4
      apps/api/src/app/portfolio/portfolio.controller.ts
  3. 30
      apps/api/src/app/portfolio/portfolio.service.ts
  4. 31
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
  5. 46
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
  6. 4
      apps/client/src/app/services/data.service.ts
  7. 4
      libs/common/src/lib/interfaces/index.ts
  8. 5
      libs/common/src/lib/interfaces/portfolio-report.interface.ts
  9. 9
      libs/common/src/lib/interfaces/responses/portfolio-report.interface.ts

6
CHANGELOG.md

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Extended the _X-ray_ page by a summary
## 2.126.1 - 2024-12-07 ## 2.126.1 - 2024-12-07
### Added ### Added

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

@ -23,7 +23,7 @@ import {
PortfolioHoldingsResponse, PortfolioHoldingsResponse,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioReport PortfolioReportResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { import {
hasReadRestrictedAccessPermission, hasReadRestrictedAccessPermission,
@ -611,7 +611,7 @@ export class PortfolioController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getReport( public async getReport(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string
): Promise<PortfolioReport> { ): Promise<PortfolioReportResponse> {
const report = await this.portfolioService.getReport(impersonationId); const report = await this.portfolioService.getReport(impersonationId);
if ( if (

30
apps/api/src/app/portfolio/portfolio.service.ts

@ -37,7 +37,7 @@ import {
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPosition, PortfolioPosition,
PortfolioReport, PortfolioReportResponse,
PortfolioSummary, PortfolioSummary,
Position, Position,
UserSettings UserSettings
@ -1162,7 +1162,9 @@ export class PortfolioService {
}; };
} }
public async getReport(impersonationId: string): Promise<PortfolioReport> { public async getReport(
impersonationId: string
): Promise<PortfolioReportResponse> {
const userId = await this.getUserId(impersonationId, this.request.user.id); const userId = await this.getUserId(impersonationId, this.request.user.id);
const userSettings = this.request.user.Settings.settings as UserSettings; const userSettings = this.request.user.Settings.settings as UserSettings;
@ -1179,8 +1181,7 @@ export class PortfolioService {
}) })
).toNumber(); ).toNumber();
return { const rules: PortfolioReportResponse['rules'] = {
rules: {
accountClusterRisk: accountClusterRisk:
summary.ordersCount > 0 summary.ordersCount > 0
? await this.rulesService.evaluate( ? await this.rulesService.evaluate(
@ -1250,8 +1251,9 @@ export class PortfolioService {
], ],
userSettings userSettings
) )
}
}; };
return { rules, statistics: this.getReportStatistics(rules) };
} }
public async updateTags({ public async updateTags({
@ -1670,6 +1672,24 @@ export class PortfolioService {
return { markets, marketsAdvanced }; return { markets, marketsAdvanced };
} }
private getReportStatistics(
evaluatedRules: PortfolioReportResponse['rules']
): PortfolioReportResponse['statistics'] {
const rulesActiveCount = Object.values(evaluatedRules)
.flat()
.filter(({ isActive }) => {
return isActive === true;
}).length;
const rulesFulfilledCount = Object.values(evaluatedRules)
.flat()
.filter(({ value }) => {
return value === true;
}).length;
return { rulesActiveCount, rulesFulfilledCount };
}
private getStreaks({ private getStreaks({
investments, investments,
savingsRate savingsRate

31
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html

@ -2,11 +2,28 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h2 class="d-none d-sm-block h3 mb-3 text-center">X-ray</h2> <h2 class="d-none d-sm-block h3 mb-3 text-center">X-ray</h2>
<p class="mb-4" i18n> <p i18n>
Ghostfolio X-ray uses static analysis to uncover potential issues and Ghostfolio X-ray uses static analysis to uncover potential issues and
risks in your portfolio. Adjust the rules below and set custom risks in your portfolio. Adjust the rules below and set custom
thresholds to align with your personal investment strategy. thresholds to align with your personal investment strategy.
</p> </p>
<p class="mb-4">
@if (isLoading) {
<ngx-skeleton-loader
animation="pulse"
class="w-100"
[theme]="{
height: '1rem',
width: '100%'
}"
/>
} @else {
{{ statistics?.rulesFulfilledCount }}
<ng-container i18n>of</ng-container>
{{ statistics?.rulesActiveCount }}
<ng-container i18n>rules are currently fulfilled.</ng-container>
}
</p>
<div class="mb-4"> <div class="mb-4">
<h4 class="align-items-center d-flex m-0"> <h4 class="align-items-center d-flex m-0">
<span i18n>Emergency Fund</span> <span i18n>Emergency Fund</span>
@ -20,7 +37,7 @@
hasPermissionToUpdateUserSettings && hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures user?.settings?.isExperimentalFeatures
" "
[isLoading]="isLoadingPortfolioReport" [isLoading]="isLoading"
[rules]="emergencyFundRules" [rules]="emergencyFundRules"
[settings]="user?.settings?.xRayRules" [settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)" (rulesUpdated)="onRulesUpdated($event)"
@ -39,7 +56,7 @@
hasPermissionToUpdateUserSettings && hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures user?.settings?.isExperimentalFeatures
" "
[isLoading]="isLoadingPortfolioReport" [isLoading]="isLoading"
[rules]="currencyClusterRiskRules" [rules]="currencyClusterRiskRules"
[settings]="user?.settings?.xRayRules" [settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)" (rulesUpdated)="onRulesUpdated($event)"
@ -58,7 +75,7 @@
hasPermissionToUpdateUserSettings && hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures user?.settings?.isExperimentalFeatures
" "
[isLoading]="isLoadingPortfolioReport" [isLoading]="isLoading"
[rules]="accountClusterRiskRules" [rules]="accountClusterRiskRules"
[settings]="user?.settings?.xRayRules" [settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)" (rulesUpdated)="onRulesUpdated($event)"
@ -77,7 +94,7 @@
hasPermissionToUpdateUserSettings && hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures user?.settings?.isExperimentalFeatures
" "
[isLoading]="isLoadingPortfolioReport" [isLoading]="isLoading"
[rules]="economicMarketClusterRiskRules" [rules]="economicMarketClusterRiskRules"
[settings]="user?.settings?.xRayRules" [settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)" (rulesUpdated)="onRulesUpdated($event)"
@ -96,7 +113,7 @@
hasPermissionToUpdateUserSettings && hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures user?.settings?.isExperimentalFeatures
" "
[isLoading]="isLoadingPortfolioReport" [isLoading]="isLoading"
[rules]="feeRules" [rules]="feeRules"
[settings]="user?.settings?.xRayRules" [settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)" (rulesUpdated)="onRulesUpdated($event)"
@ -111,7 +128,7 @@
hasPermissionToUpdateUserSettings && hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures user?.settings?.isExperimentalFeatures
" "
[isLoading]="isLoadingPortfolioReport" [isLoading]="isLoading"
[rules]="inactiveRules" [rules]="inactiveRules"
[settings]="user?.settings?.xRayRules" [settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)" (rulesUpdated)="onRulesUpdated($event)"

46
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts

@ -3,8 +3,8 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
PortfolioReportRule, PortfolioReportResponse,
PortfolioReport PortfolioReportRule
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { User } from '@ghostfolio/common/interfaces/user.interface'; import { User } from '@ghostfolio/common/interfaces/user.interface';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -26,7 +26,8 @@ export class XRayPageComponent {
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionToUpdateUserSettings: boolean; public hasPermissionToUpdateUserSettings: boolean;
public inactiveRules: PortfolioReportRule[]; public inactiveRules: PortfolioReportRule[];
public isLoadingPortfolioReport = false; public isLoading = false;
public statistics: PortfolioReportResponse['statistics'];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -87,56 +88,53 @@ export class XRayPageComponent {
} }
private initializePortfolioReport() { private initializePortfolioReport() {
this.isLoadingPortfolioReport = true; this.isLoading = true;
this.dataService this.dataService
.fetchPortfolioReport() .fetchPortfolioReport()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((portfolioReport) => { .subscribe(({ rules, statistics }) => {
this.inactiveRules = this.mergeInactiveRules(portfolioReport); this.inactiveRules = this.mergeInactiveRules(rules);
this.statistics = statistics;
this.accountClusterRiskRules = this.accountClusterRiskRules =
portfolioReport.rules['accountClusterRisk']?.filter( rules['accountClusterRisk']?.filter(({ isActive }) => {
({ isActive }) => {
return isActive; return isActive;
} }) ?? null;
) ?? null;
this.currencyClusterRiskRules = this.currencyClusterRiskRules =
portfolioReport.rules['currencyClusterRisk']?.filter( rules['currencyClusterRisk']?.filter(({ isActive }) => {
({ isActive }) => {
return isActive; return isActive;
} }) ?? null;
) ?? null;
this.economicMarketClusterRiskRules = this.economicMarketClusterRiskRules =
portfolioReport.rules['economicMarketClusterRisk']?.filter( rules['economicMarketClusterRisk']?.filter(({ isActive }) => {
({ isActive }) => {
return isActive; return isActive;
} }) ?? null;
) ?? null;
this.emergencyFundRules = this.emergencyFundRules =
portfolioReport.rules['emergencyFund']?.filter(({ isActive }) => { rules['emergencyFund']?.filter(({ isActive }) => {
return isActive; return isActive;
}) ?? null; }) ?? null;
this.feeRules = this.feeRules =
portfolioReport.rules['fees']?.filter(({ isActive }) => { rules['fees']?.filter(({ isActive }) => {
return isActive; return isActive;
}) ?? null; }) ?? null;
this.isLoadingPortfolioReport = false; this.isLoading = false;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }
private mergeInactiveRules(report: PortfolioReport): PortfolioReportRule[] { private mergeInactiveRules(
rules: PortfolioReportResponse['rules']
): PortfolioReportRule[] {
let inactiveRules: PortfolioReportRule[] = []; let inactiveRules: PortfolioReportRule[] = [];
for (const category in report.rules) { for (const category in rules) {
const rulesArray = report.rules[category]; const rulesArray = rules[category];
inactiveRules = inactiveRules.concat( inactiveRules = inactiveRules.concat(
rulesArray.filter(({ isActive }) => { rulesArray.filter(({ isActive }) => {

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

@ -37,7 +37,7 @@ import {
PortfolioHoldingsResponse, PortfolioHoldingsResponse,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioReport, PortfolioReportResponse,
PublicPortfolioResponse, PublicPortfolioResponse,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -613,7 +613,7 @@ export class DataService {
} }
public fetchPortfolioReport() { public fetchPortfolioReport() {
return this.http.get<PortfolioReport>('/api/v1/portfolio/report'); return this.http.get<PortfolioReportResponse>('/api/v1/portfolio/report');
} }
public fetchPublicPortfolio(aAccessId: string) { public fetchPublicPortfolio(aAccessId: string) {

4
libs/common/src/lib/interfaces/index.ts

@ -34,7 +34,6 @@ 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 { PortfolioReportRule } from './portfolio-report-rule.interface'; import type { PortfolioReportRule } from './portfolio-report-rule.interface';
import type { PortfolioReport } from './portfolio-report.interface';
import type { PortfolioSummary } from './portfolio-summary.interface'; import type { PortfolioSummary } from './portfolio-summary.interface';
import type { Position } from './position.interface'; import type { Position } from './position.interface';
import type { Product } from './product'; import type { Product } from './product';
@ -50,6 +49,7 @@ import type { LookupResponse } from './responses/lookup-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 { PortfolioReportResponse } from './responses/portfolio-report.interface';
import type { PublicPortfolioResponse } from './responses/public-portfolio-response.interface'; import type { PublicPortfolioResponse } from './responses/public-portfolio-response.interface';
import type { QuotesResponse } from './responses/quotes-response.interface'; import type { QuotesResponse } from './responses/quotes-response.interface';
import type { ScraperConfiguration } from './scraper-configuration.interface'; import type { ScraperConfiguration } from './scraper-configuration.interface';
@ -108,7 +108,7 @@ export {
PortfolioPerformance, PortfolioPerformance,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPosition, PortfolioPosition,
PortfolioReport, PortfolioReportResponse,
PortfolioReportRule, PortfolioReportRule,
PortfolioSummary, PortfolioSummary,
Position, Position,

5
libs/common/src/lib/interfaces/portfolio-report.interface.ts

@ -1,5 +0,0 @@
import { PortfolioReportRule } from './portfolio-report-rule.interface';
export interface PortfolioReport {
rules: { [group: string]: PortfolioReportRule[] };
}

9
libs/common/src/lib/interfaces/responses/portfolio-report.interface.ts

@ -0,0 +1,9 @@
import { PortfolioReportRule } from '../portfolio-report-rule.interface';
export interface PortfolioReportResponse {
rules: { [group: string]: PortfolioReportRule[] };
statistics: {
rulesActiveCount: number;
rulesFulfilledCount: number;
};
}
Loading…
Cancel
Save