diff --git a/CHANGELOG.md b/CHANGELOG.md index 90603e0f..291e4ff4 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: _Regional Market Cluster Risk_ (Emerging Markets) - Added a new static portfolio analysis rule: _Regional Market Cluster Risk_ (Europe) - Added a link to _Duck.ai_ to the _Copy AI prompt to clipboard_ action on the analysis page (experimental) - Extracted the tags selector to a reusable component used in the create or update activity dialog and holding detail dialog diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index bbe2a6ca..72568f37 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -15,6 +15,7 @@ import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/model import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets'; import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup'; import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment'; +import { RegionalMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/emerging-markets'; import { RegionalMarketClusterRiskEurope } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/europe'; import { RegionalMarketClusterRiskNorthAmerica } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/north-america'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; @@ -1279,6 +1280,11 @@ export class PortfolioService { summary.ordersCount > 0 ? await this.rulesService.evaluate( [ + new RegionalMarketClusterRiskEmergingMarkets( + this.exchangeRateDataService, + marketsAdvancedTotalInBaseCurrency, + marketsAdvanced.emergingMarkets.valueInBaseCurrency + ), new RegionalMarketClusterRiskEurope( this.exchangeRateDataService, marketsAdvancedTotalInBaseCurrency, diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 44a29e73..1dae9d45 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -13,6 +13,7 @@ import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/model import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets'; import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup'; import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment'; +import { RegionalMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/emerging-markets'; import { RegionalMarketClusterRiskEurope } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/europe'; import { RegionalMarketClusterRiskNorthAmerica } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/north-america'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; @@ -271,6 +272,12 @@ export class UserService { undefined, undefined ).getSettings(user.Settings.settings), + RegionalMarketClusterRiskEmergingMarkets: + new RegionalMarketClusterRiskEmergingMarkets( + undefined, + undefined, + undefined + ).getSettings(user.Settings.settings), RegionalMarketClusterRiskEurope: new RegionalMarketClusterRiskEurope( undefined, undefined, diff --git a/apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts b/apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts new file mode 100644 index 00000000..2d9a3b39 --- /dev/null +++ b/apps/api/src/models/rules/regional-market-cluster-risk/emerging-markets.ts @@ -0,0 +1,79 @@ +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'; + +import { Settings } from './interfaces/rule-settings.interface'; + +export class RegionalMarketClusterRiskEmergingMarkets extends Rule { + private currentValueInBaseCurrency: number; + private emergingMarketsValueInBaseCurrency: number; + + public constructor( + protected exchangeRateDataService: ExchangeRateDataService, + currentValueInBaseCurrency: number, + emergingMarketsValueInBaseCurrency: number + ) { + super(exchangeRateDataService, { + key: RegionalMarketClusterRiskEmergingMarkets.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.12, + thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0.08 + }; + } +} diff --git a/libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts b/libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts index f1ca5c68..4a31ef7b 100644 --- a/libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts +++ b/libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts @@ -9,6 +9,7 @@ export interface XRayRulesSettings { EconomicMarketClusterRiskEmergingMarkets?: RuleSettings; EmergencyFundSetup?: RuleSettings; FeeRatioInitialInvestment?: RuleSettings; + RegionalMarketClusterRiskEmergingMarkets?: RuleSettings; RegionalMarketClusterRiskEurope?: RuleSettings; RegionalMarketClusterRiskNorthAmerica?: RuleSettings; }