diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f15ad40a..a282de2b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added a new static portfolio analysis rule based on the total investment volume: _Fees_ (Fee Ratio) - Extended the content of the _Self-Hosting_ section on the Frequently Asked Questions (FAQ) page with information on derived currencies ### Changed +- Deprecated the existing static portfolio analysis rule: _Fees_ (Fee Ratio) - Ignored nested ETFs when fetching top holdings for ETF and mutual fund assets from _Yahoo Finance_ - Improved the scraper configuration with more detailed error messages - Upgraded `cheerio` from version `1.0.0` to `1.2.0` diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index a7cee0bc0..b96f5ef70 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -14,6 +14,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 { FeeRatioTotalInvestmentVolume } from '@ghostfolio/api/models/rules/fees/fee-ratio-total-investment-volume'; 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'; @@ -1315,6 +1316,13 @@ export class PortfolioService { userSettings.language, summary.committedFunds, summary.fees + ), + new FeeRatioTotalInvestmentVolume( + this.exchangeRateDataService, + this.i18nService, + userSettings.language, + summary.totalBuy + summary.totalSell, + summary.fees ) ], userSettings diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index def0b94d9..08328851d 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 { FeeRatioTotalInvestmentVolume } from '@ghostfolio/api/models/rules/fees/fee-ratio-total-investment-volume'; 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'; @@ -383,6 +384,13 @@ export class UserService { undefined, undefined ).getSettings(user.settings.settings), + FeeRatioTotalInvestmentVolume: new FeeRatioTotalInvestmentVolume( + undefined, + undefined, + undefined, + undefined, + undefined + ).getSettings(user.settings.settings), RegionalMarketClusterRiskAsiaPacific: new RegionalMarketClusterRiskAsiaPacific( undefined, diff --git a/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts b/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts index cb85a73ba..54c2decc9 100644 --- a/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts +++ b/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts @@ -3,6 +3,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; import { RuleSettings, UserSettings } from '@ghostfolio/common/interfaces'; +/** + * @deprecated This rule is deprecated in favor of FeeRatioTotalInvestmentVolume + */ export class FeeRatioInitialInvestment extends Rule { private fees: number; private totalInvestment: number; diff --git a/apps/api/src/models/rules/fees/fee-ratio-total-investment-volume.ts b/apps/api/src/models/rules/fees/fee-ratio-total-investment-volume.ts new file mode 100644 index 000000000..07bf5fa2c --- /dev/null +++ b/apps/api/src/models/rules/fees/fee-ratio-total-investment-volume.ts @@ -0,0 +1,102 @@ +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 { RuleSettings, UserSettings } from '@ghostfolio/common/interfaces'; + +export class FeeRatioTotalInvestmentVolume extends Rule { + private fees: number; + private totalInvestmentVolumeInBaseCurrency: number; + + public constructor( + protected exchangeRateDataService: ExchangeRateDataService, + private i18nService: I18nService, + languageCode: string, + totalInvestmentVolumeInBaseCurrency: number, + fees: number + ) { + super(exchangeRateDataService, { + languageCode, + key: FeeRatioTotalInvestmentVolume.name + }); + + this.fees = fees; + this.totalInvestmentVolumeInBaseCurrency = + totalInvestmentVolumeInBaseCurrency; + } + + public evaluate(ruleSettings: Settings) { + const feeRatio = this.totalInvestmentVolumeInBaseCurrency + ? this.fees / this.totalInvestmentVolumeInBaseCurrency + : 0; + + if (feeRatio > ruleSettings.thresholdMax) { + return { + evaluation: this.i18nService.getTranslation({ + id: 'rule.feeRatioTotalInvestmentVolume.false', + languageCode: this.getLanguageCode(), + placeholders: { + feeRatio: (ruleSettings.thresholdMax * 100).toFixed(2), + thresholdMax: (feeRatio * 100).toPrecision(3) + } + }), + value: false + }; + } + + return { + evaluation: this.i18nService.getTranslation({ + id: 'rule.feeRatioTotalInvestmentVolume.true', + languageCode: this.getLanguageCode(), + placeholders: { + feeRatio: (feeRatio * 100).toPrecision(3), + thresholdMax: (ruleSettings.thresholdMax * 100).toFixed(2) + } + }), + value: true + }; + } + + public getCategoryName() { + return this.i18nService.getTranslation({ + id: 'rule.fees.category', + languageCode: this.getLanguageCode() + }); + } + + public getConfiguration() { + return { + threshold: { + max: 0.1, + min: 0, + step: 0.0025, + unit: '%' + }, + thresholdMax: true + }; + } + + public getName() { + return this.i18nService.getTranslation({ + id: 'rule.feeRatioTotalInvestmentVolume', + languageCode: this.getLanguageCode() + }); + } + + public getSettings({ + baseCurrency, + locale, + xRayRules + }: UserSettings): Settings { + return { + baseCurrency, + locale, + isActive: xRayRules?.[this.getKey()]?.isActive ?? true, + thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.01 + }; + } +} + +interface Settings extends RuleSettings { + baseCurrency: string; + thresholdMax: number; +} diff --git a/apps/client/src/app/pages/i18n/i18n-page.html b/apps/client/src/app/pages/i18n/i18n-page.html index b4297d5ac..8ccd27b64 100644 --- a/apps/client/src/app/pages/i18n/i18n-page.html +++ b/apps/client/src/app/pages/i18n/i18n-page.html @@ -149,7 +149,7 @@
  • An emergency fund has been set up
  • -
  • Fee Ratio
  • +
  • Fee Ratio (lecacy)
  • The fees do exceed ${thresholdMax}% of your initial investment (${feeRatio}%) @@ -158,6 +158,15 @@ The fees do not exceed ${thresholdMax}% of your initial investment (${feeRatio}%)
  • +
  • Fee Ratio
  • +
  • + The fees do exceed ${thresholdMax}% of your total investment + volume (${feeRatio}%) +
  • +
  • + The fees do not exceed ${thresholdMax}% of your total + investment volume (${feeRatio}%) +
  • Fees
  • Regional Market Cluster Risks 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 bdef3e169..688d4f2a0 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 @@ -10,6 +10,7 @@ export interface XRayRulesSettings { EconomicMarketClusterRiskEmergingMarkets?: RuleSettings; EmergencyFundSetup?: RuleSettings; FeeRatioInitialInvestment?: RuleSettings; + FeeRatioTotalInvestmentVolume?: RuleSettings; RegionalMarketClusterRiskAsiaPacific?: RuleSettings; RegionalMarketClusterRiskEmergingMarkets?: RuleSettings; RegionalMarketClusterRiskEurope?: RuleSettings;