From 52ea3c5e8478c90493853698ccc559f4b080229f Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 14 Aug 2025 20:48:56 +0200 Subject: [PATCH] Feature/add buying power to static portfolio analysis rules (#5365) * Add buying power rule * Update changelog --- CHANGELOG.md | 1 + .../src/app/portfolio/portfolio.service.ts | 12 +++ apps/api/src/app/user/user.service.ts | 7 ++ .../models/rules/liquidity/buying-power.ts | 90 +++++++++++++++++++ apps/client/src/app/pages/i18n/i18n-page.html | 10 +++ .../portfolio/x-ray/x-ray-page.component.html | 24 +++++ .../portfolio/x-ray/x-ray-page.component.ts | 6 ++ .../x-ray-rules-settings.interface.ts | 1 + 8 files changed, 151 insertions(+) create mode 100644 apps/api/src/models/rules/liquidity/buying-power.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c6182b11..d71772e8f 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: _Liquidity_ (Buying Power) - Added the interest and dividend values to the account detail dialog ### Changed diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index c15d06c1e..f40ed57e5 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 { BuyingPower } from '@ghostfolio/api/models/rules/liquidity/buying-power'; import { RegionalMarketClusterRiskAsiaPacific } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/asia-pacific'; 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'; @@ -1331,6 +1332,17 @@ export class PortfolioService { ], userSettings ), + liquidity: await this.rulesService.evaluate( + [ + new BuyingPower( + this.exchangeRateDataService, + this.i18nService, + summary.cash, + userSettings.language + ) + ], + userSettings + ), regionalMarketClusterRisk: summary.activityCount > 0 ? await this.rulesService.evaluate( diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 405c4c0b0..a84e10c44 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 { BuyingPower } from '@ghostfolio/api/models/rules/liquidity/buying-power'; import { RegionalMarketClusterRiskAsiaPacific } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/asia-pacific'; 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'; @@ -287,6 +288,12 @@ export class UserService { undefined, undefined ).getSettings(user.settings.settings), + BuyingPower: new BuyingPower( + undefined, + undefined, + undefined, + undefined + ).getSettings(user.settings.settings), CurrencyClusterRiskBaseCurrencyCurrentInvestment: new CurrencyClusterRiskBaseCurrencyCurrentInvestment( undefined, diff --git a/apps/api/src/models/rules/liquidity/buying-power.ts b/apps/api/src/models/rules/liquidity/buying-power.ts new file mode 100644 index 000000000..a8b74f354 --- /dev/null +++ b/apps/api/src/models/rules/liquidity/buying-power.ts @@ -0,0 +1,90 @@ +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 { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; +import { UserSettings } from '@ghostfolio/common/interfaces'; + +export class BuyingPower extends Rule { + private buyingPower: number; + + public constructor( + protected exchangeRateDataService: ExchangeRateDataService, + private i18nService: I18nService, + buyingPower: number, + languageCode: string + ) { + super(exchangeRateDataService, { + languageCode, + key: BuyingPower.name + }); + + this.buyingPower = buyingPower; + } + + public evaluate(ruleSettings: Settings) { + if (this.buyingPower < ruleSettings.thresholdMin) { + return { + evaluation: this.i18nService.getTranslation({ + id: 'rule.liquidityBuyingPower.false', + languageCode: this.getLanguageCode(), + placeholders: { + baseCurrency: ruleSettings.baseCurrency, + thresholdMin: ruleSettings.thresholdMin + } + }), + value: false + }; + } + + return { + evaluation: this.i18nService.getTranslation({ + id: 'rule.liquidityBuyingPower.true', + languageCode: this.getLanguageCode(), + placeholders: { + baseCurrency: ruleSettings.baseCurrency, + thresholdMin: ruleSettings.thresholdMin + } + }), + value: true + }; + } + + public getCategoryName() { + return this.i18nService.getTranslation({ + id: 'rule.liquidity.category', + languageCode: this.getLanguageCode() + }); + } + + public getConfiguration() { + return { + threshold: { + max: 200000, + min: 0, + step: 1000, + unit: '' + }, + thresholdMin: true + }; + } + + public getName() { + return this.i18nService.getTranslation({ + id: 'rule.liquidityBuyingPower', + languageCode: this.getLanguageCode() + }); + } + + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { + return { + baseCurrency, + isActive: xRayRules?.[this.getKey()].isActive ?? true, + thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0 + }; + } +} + +interface Settings extends RuleSettings { + baseCurrency: string; + thresholdMin: number; +} diff --git a/apps/client/src/app/pages/i18n/i18n-page.html b/apps/client/src/app/pages/i18n/i18n-page.html index 5db40a5b0..f42412473 100644 --- a/apps/client/src/app/pages/i18n/i18n-page.html +++ b/apps/client/src/app/pages/i18n/i18n-page.html @@ -67,6 +67,16 @@ (${fixedIncomeValueRatio}%) is within the range of ${thresholdMin}% and ${thresholdMax}% +
  • Liquidity
  • +
  • Buying Power
  • +
  • + Your buying power is below ${thresholdMin} + ${baseCurrency} +
  • +
  • + Your buying power exceeds ${thresholdMin} + ${baseCurrency} +
  • Currency Cluster Risks
  • Investment: Base Currency diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html index 24ca0f474..e309fbaea 100644 --- a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html @@ -59,6 +59,30 @@ } +
    +

    + Liquidity + @if (user?.subscription?.type === 'Basic') { + + } +

    + +