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

* Add summary to X-ray page

* Update changelog
pull/4111/head
Thomas Kaul 2 weeks ago committed by GitHub
parent d6357487ea
commit 291be3e605
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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,79 +1181,79 @@ export class PortfolioService {
})
).toNumber();
return {
rules: {
accountClusterRisk:
summary.ordersCount > 0
? await this.rulesService.evaluate(
[
new AccountClusterRiskCurrentInvestment(
this.exchangeRateDataService,
accounts
),
new AccountClusterRiskSingleAccount(
this.exchangeRateDataService,
accounts
)
],
userSettings
)
: undefined,
economicMarketClusterRisk:
summary.ordersCount > 0
? await this.rulesService.evaluate(
[
new EconomicMarketClusterRiskDevelopedMarkets(
this.exchangeRateDataService,
marketsTotalInBaseCurrency,
markets.developedMarkets.valueInBaseCurrency
),
new EconomicMarketClusterRiskEmergingMarkets(
this.exchangeRateDataService,
marketsTotalInBaseCurrency,
markets.emergingMarkets.valueInBaseCurrency
)
],
userSettings
)
: undefined,
currencyClusterRisk:
summary.ordersCount > 0
? await this.rulesService.evaluate(
[
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
this.exchangeRateDataService,
Object.values(holdings)
),
new CurrencyClusterRiskCurrentInvestment(
this.exchangeRateDataService,
Object.values(holdings)
)
],
userSettings
)
: undefined,
emergencyFund: await this.rulesService.evaluate(
[
new EmergencyFundSetup(
this.exchangeRateDataService,
userSettings.emergencyFund
const rules: PortfolioReportResponse['rules'] = {
accountClusterRisk:
summary.ordersCount > 0
? await this.rulesService.evaluate(
[
new AccountClusterRiskCurrentInvestment(
this.exchangeRateDataService,
accounts
),
new AccountClusterRiskSingleAccount(
this.exchangeRateDataService,
accounts
)
],
userSettings
)
],
userSettings
),
fees: await this.rulesService.evaluate(
[
new FeeRatioInitialInvestment(
this.exchangeRateDataService,
summary.committedFunds,
summary.fees
: undefined,
economicMarketClusterRisk:
summary.ordersCount > 0
? await this.rulesService.evaluate(
[
new EconomicMarketClusterRiskDevelopedMarkets(
this.exchangeRateDataService,
marketsTotalInBaseCurrency,
markets.developedMarkets.valueInBaseCurrency
),
new EconomicMarketClusterRiskEmergingMarkets(
this.exchangeRateDataService,
marketsTotalInBaseCurrency,
markets.emergingMarkets.valueInBaseCurrency
)
],
userSettings
)
],
userSettings
)
}
: undefined,
currencyClusterRisk:
summary.ordersCount > 0
? await this.rulesService.evaluate(
[
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
this.exchangeRateDataService,
Object.values(holdings)
),
new CurrencyClusterRiskCurrentInvestment(
this.exchangeRateDataService,
Object.values(holdings)
)
],
userSettings
)
: undefined,
emergencyFund: await this.rulesService.evaluate(
[
new EmergencyFundSetup(
this.exchangeRateDataService,
userSettings.emergencyFund
)
],
userSettings
),
fees: await this.rulesService.evaluate(
[
new FeeRatioInitialInvestment(
this.exchangeRateDataService,
summary.committedFunds,
summary.fees
)
],
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 }) => {
return isActive;
}
) ?? null;
rules['accountClusterRisk']?.filter(({ isActive }) => {
return isActive;
}) ?? null;
this.currencyClusterRiskRules =
portfolioReport.rules['currencyClusterRisk']?.filter(
({ isActive }) => {
return isActive;
}
) ?? null;
rules['currencyClusterRisk']?.filter(({ isActive }) => {
return isActive;
}) ?? null;
this.economicMarketClusterRiskRules =
portfolioReport.rules['economicMarketClusterRisk']?.filter(
({ isActive }) => {
return isActive;
}
) ?? null;
rules['economicMarketClusterRisk']?.filter(({ isActive }) => {
return isActive;
}) ?? 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…
Cancel
Save