diff --git a/CHANGELOG.md b/CHANGELOG.md index f5b337f55..fcdf3dc6d 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 (Developed Markets) - Added a new static portfolio analysis rule: Allocation Cluster Risk (Emerging Markets) - Added support for mutual funds in the _EOD Historical Data_ service diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 1835a2215..ea366304a 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 { AllocationClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/allocation-cluster-risk/developed-markets'; 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'; @@ -1190,6 +1191,11 @@ export class PortfolioService { summary.ordersCount > 0 ? await this.rulesService.evaluate( [ + new AllocationClusterRiskDevelopedMarkets( + this.exchangeRateDataService, + summary.currentValueInBaseCurrency, + markets.developedMarkets.valueInBaseCurrency + ), new AllocationClusterRiskEmergingMarkets( this.exchangeRateDataService, summary.currentValueInBaseCurrency, diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index f4f0dad33..d0b7ab983 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 { AllocationClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/allocation-cluster-risk/developed-markets'; 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'; @@ -216,6 +217,12 @@ export class UserService { undefined, {} ).getSettings(user.Settings.settings), + AllocationClusterRiskDevelopedMarkets: + new AllocationClusterRiskDevelopedMarkets( + undefined, + undefined, + undefined + ).getSettings(user.Settings.settings), AllocationClusterRiskEmergingMarkets: new AllocationClusterRiskEmergingMarkets( undefined, diff --git a/apps/api/src/models/rules/allocation-cluster-risk/developed-markets.ts b/apps/api/src/models/rules/allocation-cluster-risk/developed-markets.ts new file mode 100644 index 000000000..068ebdda3 --- /dev/null +++ b/apps/api/src/models/rules/allocation-cluster-risk/developed-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 AllocationClusterRiskDevelopedMarkets extends Rule { + private currentValueInBaseCurrency: number; + private developedMarketsValueInBaseCurrency: number; + + public constructor( + protected exchangeRateDataService: ExchangeRateDataService, + currentValueInBaseCurrency: number, + developedMarketsValueInBaseCurrency: number + ) { + super(exchangeRateDataService, { + key: AllocationClusterRiskDevelopedMarkets.name, + name: 'Developed Markets' + }); + + this.currentValueInBaseCurrency = currentValueInBaseCurrency; + this.developedMarketsValueInBaseCurrency = + developedMarketsValueInBaseCurrency; + } + + public evaluate(ruleSettings: Settings) { + const developedMarketsValueRatio = this.currentValueInBaseCurrency + ? this.developedMarketsValueInBaseCurrency / + this.currentValueInBaseCurrency + : 0; + + if (developedMarketsValueRatio > ruleSettings.thresholdMax) { + return { + evaluation: `The developed markets contribution of your current investment (${(developedMarketsValueRatio * 100).toPrecision(3)}%) exceeds ${( + ruleSettings.thresholdMax * 100 + ).toPrecision(3)}%`, + value: false + }; + } else if (developedMarketsValueRatio < ruleSettings.thresholdMin) { + return { + evaluation: `The developed markets contribution of your current investment (${(developedMarketsValueRatio * 100).toPrecision(3)}%) is below ${( + ruleSettings.thresholdMin * 100 + ).toPrecision(3)}%`, + value: false + }; + } + + return { + evaluation: `The developed markets contribution of your current investment (${(developedMarketsValueRatio * 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.72, + thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.68 + }; + } +} + +interface Settings extends RuleSettings { + baseCurrency: string; + thresholdMin: number; + thresholdMax: number; +} 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 ffaff41a9..8808162f5 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; + AllocationClusterRiskDevelopedMarkets?: RuleSettings; AllocationClusterRiskEmergingMarkets?: RuleSettings; CurrencyClusterRiskBaseCurrencyCurrentInvestment?: RuleSettings; CurrencyClusterRiskCurrentInvestment?: RuleSettings;