From d325f8bfaf0128b961fdd6f4de7e6b42baf0f72c Mon Sep 17 00:00:00 2001 From: vitalymatyushik Date: Tue, 22 Oct 2024 20:19:56 +0200 Subject: [PATCH] Feature/Add static portfolio analysis rule for emerging Markets (#3949) * Add static portfolio analysis rule: Allocation Cluster Risk (Emerging Markets) * Update changelog --- CHANGELOG.md | 1 + .../src/app/portfolio/portfolio.service.ts | 22 +++-- apps/api/src/app/user/user.service.ts | 7 ++ .../emerging-markets.ts | 84 +++++++++++++++++++ .../portfolio/fire/fire-page.component.ts | 8 ++ .../app/pages/portfolio/fire/fire-page.html | 19 +++++ .../lib/types/x-ray-rules-settings.type.ts | 1 + 7 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 apps/api/src/models/rules/allocation-cluster-risk/emerging-markets.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 485d7b508..c5e2c109d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added a new static portfolio analysis rule: Allocation Cluster Risk (Emerging Markets) - Added support for mutual funds in the _EOD Historical Data_ service ### Changed diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index bbc8f437e..1835a2215 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -7,6 +7,7 @@ import { UserService } from '@ghostfolio/api/app/user/user.service'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; +import { AllocationClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/allocation-cluster-risk/emerging-markets'; import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment'; import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment'; import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup'; @@ -1160,7 +1161,7 @@ export class PortfolioService { const userId = await this.getUserId(impersonationId, this.request.user.id); const userSettings = this.request.user.Settings.settings; - const { accounts, holdings, summary } = await this.getDetails({ + const { accounts, holdings, markets, summary } = await this.getDetails({ impersonationId, userId, withMarkets: true, @@ -1185,6 +1186,19 @@ export class PortfolioService { userSettings ) : undefined, + allocationClusterRisk: + summary.ordersCount > 0 + ? await this.rulesService.evaluate( + [ + new AllocationClusterRiskEmergingMarkets( + this.exchangeRateDataService, + summary.currentValueInBaseCurrency, + markets.emergingMarkets.valueInBaseCurrency + ) + ], + userSettings + ) + : undefined, currencyClusterRisk: summary.ordersCount > 0 ? await this.rulesService.evaluate( @@ -1242,9 +1256,7 @@ export class PortfolioService { await this.orderService.assignTags({ dataSource, symbol, tags, userId }); } - private getAggregatedMarkets(holdings: { - [symbol: string]: PortfolioPosition; - }): { + private getAggregatedMarkets(holdings: Record): { markets: PortfolioDetails['markets']; marketsAdvanced: PortfolioDetails['marketsAdvanced']; } { @@ -1903,7 +1915,7 @@ export class PortfolioService { }: { activities: Activity[]; filters?: Filter[]; - portfolioItemsNow: { [p: string]: TimelinePosition }; + portfolioItemsNow: Record; userCurrency: string; userId: string; withExcludedAccounts?: boolean; diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index e8a437be6..f4f0dad33 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -4,6 +4,7 @@ import { environment } from '@ghostfolio/api/environments/environment'; import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; +import { AllocationClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/allocation-cluster-risk/emerging-markets'; import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment'; import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment'; import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup'; @@ -215,6 +216,12 @@ export class UserService { undefined, {} ).getSettings(user.Settings.settings), + AllocationClusterRiskEmergingMarkets: + new AllocationClusterRiskEmergingMarkets( + undefined, + undefined, + undefined + ).getSettings(user.Settings.settings), CurrencyClusterRiskBaseCurrencyCurrentInvestment: new CurrencyClusterRiskBaseCurrencyCurrentInvestment( undefined, diff --git a/apps/api/src/models/rules/allocation-cluster-risk/emerging-markets.ts b/apps/api/src/models/rules/allocation-cluster-risk/emerging-markets.ts new file mode 100644 index 000000000..e7c107510 --- /dev/null +++ b/apps/api/src/models/rules/allocation-cluster-risk/emerging-markets.ts @@ -0,0 +1,84 @@ +import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; +import { Rule } from '@ghostfolio/api/models/rule'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { UserSettings } from '@ghostfolio/common/interfaces'; + +export class AllocationClusterRiskEmergingMarkets extends Rule { + private currentValueInBaseCurrency: number; + private emergingMarketsValueInBaseCurrency: number; + + public constructor( + protected exchangeRateDataService: ExchangeRateDataService, + currentValueInBaseCurrency: number, + emergingMarketsValueInBaseCurrency: number + ) { + super(exchangeRateDataService, { + key: AllocationClusterRiskEmergingMarkets.name, + name: 'Emerging Markets' + }); + + this.currentValueInBaseCurrency = currentValueInBaseCurrency; + this.emergingMarketsValueInBaseCurrency = + emergingMarketsValueInBaseCurrency; + } + + public evaluate(ruleSettings: Settings) { + const emergingMarketsValueRatio = this.currentValueInBaseCurrency + ? this.emergingMarketsValueInBaseCurrency / + this.currentValueInBaseCurrency + : 0; + + if (emergingMarketsValueRatio > ruleSettings.thresholdMax) { + return { + evaluation: `The emerging markets contribution of your current investment (${(emergingMarketsValueRatio * 100).toPrecision(3)}%) exceeds ${( + ruleSettings.thresholdMax * 100 + ).toPrecision(3)}%`, + value: false + }; + } else if (emergingMarketsValueRatio < ruleSettings.thresholdMin) { + return { + evaluation: `The emerging markets contribution of your current investment (${(emergingMarketsValueRatio * 100).toPrecision(3)}%) is below ${( + ruleSettings.thresholdMin * 100 + ).toPrecision(3)}%`, + value: false + }; + } + + return { + evaluation: `The emerging markets contribution of your current investment (${(emergingMarketsValueRatio * 100).toPrecision(3)}%) is within the range of ${( + ruleSettings.thresholdMin * 100 + ).toPrecision( + 3 + )}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`, + value: true + }; + } + + public getConfiguration() { + return { + threshold: { + max: 1, + min: 0, + step: 0.01, + unit: '%' + }, + thresholdMax: true, + thresholdMin: true + }; + } + + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { + return { + baseCurrency, + isActive: xRayRules?.[this.getKey()]?.isActive ?? true, + thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.32, + thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.28 + }; + } +} + +interface Settings extends RuleSettings { + baseCurrency: string; + thresholdMin: number; + thresholdMax: number; +} diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index 54f65b531..ea83500c4 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -22,6 +22,7 @@ import { takeUntil } from 'rxjs/operators'; }) export class FirePageComponent implements OnDestroy, OnInit { public accountClusterRiskRules: PortfolioReportRule[]; + public allocationClusterRiskRules: PortfolioReportRule[]; public currencyClusterRiskRules: PortfolioReportRule[]; public deviceType: string; public emergencyFundRules: PortfolioReportRule[]; @@ -203,6 +204,13 @@ export class FirePageComponent implements OnDestroy, OnInit { } ) ?? null; + this.allocationClusterRiskRules = + portfolioReport.rules['allocationClusterRisk']?.filter( + ({ isActive }) => { + return isActive; + } + ) ?? null; + this.currencyClusterRiskRules = portfolioReport.rules['currencyClusterRisk']?.filter( ({ isActive }) => { diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html index c4a521a8c..4eedca300 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -174,6 +174,25 @@ (rulesUpdated)="onRulesUpdated($event)" /> +
+

+ Allocation Cluster Risks + @if (user?.subscription?.type === 'Basic') { + + } +

+ +

Fees diff --git a/libs/common/src/lib/types/x-ray-rules-settings.type.ts b/libs/common/src/lib/types/x-ray-rules-settings.type.ts index fddd708cc..ffaff41a9 100644 --- a/libs/common/src/lib/types/x-ray-rules-settings.type.ts +++ b/libs/common/src/lib/types/x-ray-rules-settings.type.ts @@ -1,6 +1,7 @@ export type XRayRulesSettings = { AccountClusterRiskCurrentInvestment?: RuleSettings; AccountClusterRiskSingleAccount?: RuleSettings; + AllocationClusterRiskEmergingMarkets?: RuleSettings; CurrencyClusterRiskBaseCurrencyCurrentInvestment?: RuleSettings; CurrencyClusterRiskCurrentInvestment?: RuleSettings; EmergencyFundSetup?: RuleSettings;