Feature/extend X-ray page by summary (#4107)
* Add summary to X-ray page * Update changelog
This commit is contained in:
parent
d6357487ea
commit
291be3e605
@ -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/),
|
||||
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
|
||||
|
||||
### Added
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
PortfolioHoldingsResponse,
|
||||
PortfolioInvestments,
|
||||
PortfolioPerformanceResponse,
|
||||
PortfolioReport
|
||||
PortfolioReportResponse
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
hasReadRestrictedAccessPermission,
|
||||
@ -611,7 +611,7 @@ export class PortfolioController {
|
||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||
public async getReport(
|
||||
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string
|
||||
): Promise<PortfolioReport> {
|
||||
): Promise<PortfolioReportResponse> {
|
||||
const report = await this.portfolioService.getReport(impersonationId);
|
||||
|
||||
if (
|
||||
|
@ -37,7 +37,7 @@ import {
|
||||
PortfolioInvestments,
|
||||
PortfolioPerformanceResponse,
|
||||
PortfolioPosition,
|
||||
PortfolioReport,
|
||||
PortfolioReportResponse,
|
||||
PortfolioSummary,
|
||||
Position,
|
||||
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 userSettings = this.request.user.Settings.settings as UserSettings;
|
||||
|
||||
@ -1179,8 +1181,7 @@ export class PortfolioService {
|
||||
})
|
||||
).toNumber();
|
||||
|
||||
return {
|
||||
rules: {
|
||||
const rules: PortfolioReportResponse['rules'] = {
|
||||
accountClusterRisk:
|
||||
summary.ordersCount > 0
|
||||
? await this.rulesService.evaluate(
|
||||
@ -1250,8 +1251,9 @@ export class PortfolioService {
|
||||
],
|
||||
userSettings
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
return { rules, statistics: this.getReportStatistics(rules) };
|
||||
}
|
||||
|
||||
public async updateTags({
|
||||
@ -1670,6 +1672,24 @@ export class PortfolioService {
|
||||
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({
|
||||
investments,
|
||||
savingsRate
|
||||
|
@ -2,11 +2,28 @@
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<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
|
||||
risks in your portfolio. Adjust the rules below and set custom
|
||||
thresholds to align with your personal investment strategy.
|
||||
</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">
|
||||
<h4 class="align-items-center d-flex m-0">
|
||||
<span i18n>Emergency Fund</span>
|
||||
@ -20,7 +37,7 @@
|
||||
hasPermissionToUpdateUserSettings &&
|
||||
user?.settings?.isExperimentalFeatures
|
||||
"
|
||||
[isLoading]="isLoadingPortfolioReport"
|
||||
[isLoading]="isLoading"
|
||||
[rules]="emergencyFundRules"
|
||||
[settings]="user?.settings?.xRayRules"
|
||||
(rulesUpdated)="onRulesUpdated($event)"
|
||||
@ -39,7 +56,7 @@
|
||||
hasPermissionToUpdateUserSettings &&
|
||||
user?.settings?.isExperimentalFeatures
|
||||
"
|
||||
[isLoading]="isLoadingPortfolioReport"
|
||||
[isLoading]="isLoading"
|
||||
[rules]="currencyClusterRiskRules"
|
||||
[settings]="user?.settings?.xRayRules"
|
||||
(rulesUpdated)="onRulesUpdated($event)"
|
||||
@ -58,7 +75,7 @@
|
||||
hasPermissionToUpdateUserSettings &&
|
||||
user?.settings?.isExperimentalFeatures
|
||||
"
|
||||
[isLoading]="isLoadingPortfolioReport"
|
||||
[isLoading]="isLoading"
|
||||
[rules]="accountClusterRiskRules"
|
||||
[settings]="user?.settings?.xRayRules"
|
||||
(rulesUpdated)="onRulesUpdated($event)"
|
||||
@ -77,7 +94,7 @@
|
||||
hasPermissionToUpdateUserSettings &&
|
||||
user?.settings?.isExperimentalFeatures
|
||||
"
|
||||
[isLoading]="isLoadingPortfolioReport"
|
||||
[isLoading]="isLoading"
|
||||
[rules]="economicMarketClusterRiskRules"
|
||||
[settings]="user?.settings?.xRayRules"
|
||||
(rulesUpdated)="onRulesUpdated($event)"
|
||||
@ -96,7 +113,7 @@
|
||||
hasPermissionToUpdateUserSettings &&
|
||||
user?.settings?.isExperimentalFeatures
|
||||
"
|
||||
[isLoading]="isLoadingPortfolioReport"
|
||||
[isLoading]="isLoading"
|
||||
[rules]="feeRules"
|
||||
[settings]="user?.settings?.xRayRules"
|
||||
(rulesUpdated)="onRulesUpdated($event)"
|
||||
@ -111,7 +128,7 @@
|
||||
hasPermissionToUpdateUserSettings &&
|
||||
user?.settings?.isExperimentalFeatures
|
||||
"
|
||||
[isLoading]="isLoadingPortfolioReport"
|
||||
[isLoading]="isLoading"
|
||||
[rules]="inactiveRules"
|
||||
[settings]="user?.settings?.xRayRules"
|
||||
(rulesUpdated)="onRulesUpdated($event)"
|
||||
|
@ -3,8 +3,8 @@ import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import {
|
||||
PortfolioReportRule,
|
||||
PortfolioReport
|
||||
PortfolioReportResponse,
|
||||
PortfolioReportRule
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { User } from '@ghostfolio/common/interfaces/user.interface';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
@ -26,7 +26,8 @@ export class XRayPageComponent {
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToUpdateUserSettings: boolean;
|
||||
public inactiveRules: PortfolioReportRule[];
|
||||
public isLoadingPortfolioReport = false;
|
||||
public isLoading = false;
|
||||
public statistics: PortfolioReportResponse['statistics'];
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -87,56 +88,53 @@ export class XRayPageComponent {
|
||||
}
|
||||
|
||||
private initializePortfolioReport() {
|
||||
this.isLoadingPortfolioReport = true;
|
||||
this.isLoading = true;
|
||||
|
||||
this.dataService
|
||||
.fetchPortfolioReport()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((portfolioReport) => {
|
||||
this.inactiveRules = this.mergeInactiveRules(portfolioReport);
|
||||
.subscribe(({ rules, statistics }) => {
|
||||
this.inactiveRules = this.mergeInactiveRules(rules);
|
||||
this.statistics = statistics;
|
||||
|
||||
this.accountClusterRiskRules =
|
||||
portfolioReport.rules['accountClusterRisk']?.filter(
|
||||
({ isActive }) => {
|
||||
rules['accountClusterRisk']?.filter(({ isActive }) => {
|
||||
return isActive;
|
||||
}
|
||||
) ?? null;
|
||||
}) ?? null;
|
||||
|
||||
this.currencyClusterRiskRules =
|
||||
portfolioReport.rules['currencyClusterRisk']?.filter(
|
||||
({ isActive }) => {
|
||||
rules['currencyClusterRisk']?.filter(({ isActive }) => {
|
||||
return isActive;
|
||||
}
|
||||
) ?? null;
|
||||
}) ?? null;
|
||||
|
||||
this.economicMarketClusterRiskRules =
|
||||
portfolioReport.rules['economicMarketClusterRisk']?.filter(
|
||||
({ isActive }) => {
|
||||
rules['economicMarketClusterRisk']?.filter(({ isActive }) => {
|
||||
return isActive;
|
||||
}
|
||||
) ?? null;
|
||||
}) ?? null;
|
||||
|
||||
this.emergencyFundRules =
|
||||
portfolioReport.rules['emergencyFund']?.filter(({ isActive }) => {
|
||||
rules['emergencyFund']?.filter(({ isActive }) => {
|
||||
return isActive;
|
||||
}) ?? null;
|
||||
|
||||
this.feeRules =
|
||||
portfolioReport.rules['fees']?.filter(({ isActive }) => {
|
||||
rules['fees']?.filter(({ isActive }) => {
|
||||
return isActive;
|
||||
}) ?? null;
|
||||
|
||||
this.isLoadingPortfolioReport = false;
|
||||
this.isLoading = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
private mergeInactiveRules(report: PortfolioReport): PortfolioReportRule[] {
|
||||
private mergeInactiveRules(
|
||||
rules: PortfolioReportResponse['rules']
|
||||
): PortfolioReportRule[] {
|
||||
let inactiveRules: PortfolioReportRule[] = [];
|
||||
|
||||
for (const category in report.rules) {
|
||||
const rulesArray = report.rules[category];
|
||||
for (const category in rules) {
|
||||
const rulesArray = rules[category];
|
||||
|
||||
inactiveRules = inactiveRules.concat(
|
||||
rulesArray.filter(({ isActive }) => {
|
||||
|
@ -37,7 +37,7 @@ import {
|
||||
PortfolioHoldingsResponse,
|
||||
PortfolioInvestments,
|
||||
PortfolioPerformanceResponse,
|
||||
PortfolioReport,
|
||||
PortfolioReportResponse,
|
||||
PublicPortfolioResponse,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
@ -613,7 +613,7 @@ export class DataService {
|
||||
}
|
||||
|
||||
public fetchPortfolioReport() {
|
||||
return this.http.get<PortfolioReport>('/api/v1/portfolio/report');
|
||||
return this.http.get<PortfolioReportResponse>('/api/v1/portfolio/report');
|
||||
}
|
||||
|
||||
public fetchPublicPortfolio(aAccessId: string) {
|
||||
|
@ -34,7 +34,6 @@ import type { PortfolioOverview } from './portfolio-overview.interface';
|
||||
import type { PortfolioPerformance } from './portfolio-performance.interface';
|
||||
import type { PortfolioPosition } from './portfolio-position.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 { Position } from './position.interface';
|
||||
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 { PortfolioHoldingsResponse } from './responses/portfolio-holdings-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 { QuotesResponse } from './responses/quotes-response.interface';
|
||||
import type { ScraperConfiguration } from './scraper-configuration.interface';
|
||||
@ -108,7 +108,7 @@ export {
|
||||
PortfolioPerformance,
|
||||
PortfolioPerformanceResponse,
|
||||
PortfolioPosition,
|
||||
PortfolioReport,
|
||||
PortfolioReportResponse,
|
||||
PortfolioReportRule,
|
||||
PortfolioSummary,
|
||||
Position,
|
||||
|
@ -1,5 +0,0 @@
|
||||
import { PortfolioReportRule } from './portfolio-report-rule.interface';
|
||||
|
||||
export interface PortfolioReport {
|
||||
rules: { [group: string]: PortfolioReportRule[] };
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { PortfolioReportRule } from '../portfolio-report-rule.interface';
|
||||
|
||||
export interface PortfolioReportResponse {
|
||||
rules: { [group: string]: PortfolioReportRule[] };
|
||||
statistics: {
|
||||
rulesActiveCount: number;
|
||||
rulesFulfilledCount: number;
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user