diff --git a/CHANGELOG.md b/CHANGELOG.md index ef7eef724..f5b337f55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### 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 -- Improved the backgrounds of the chart of the holdings tab on the home page (experimental) - Improved the font colors of the chart of the holdings tab on the home page (experimental) +- Optimized the dialog sizes for mobile (full screen) +- Upgraded `angular` from version `18.1.1` to `18.2.8` +- Upgraded `Nx` from version `19.5.6` to `20.0.3` + +### Fixed + +- Fixed the warning `export was not found` in connection with `GetValuesParams` +- Quoted the password for the _Redis_ service `healthcheck` in the `docker-compose` files (`docker-compose.yml` and `docker-compose.build.yml`) + +## 2.117.0 - 2024-10-19 + +### Added + +- Added the logotype to the footer +- Added the data providers management to the admin control panel + +### Changed + +- Improved the backgrounds of the chart of the holdings tab on the home page (experimental) +- Improved the language localization for German (`de`) ### Fixed 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/portfolio/rules.service.ts b/apps/api/src/app/portfolio/rules.service.ts index 5f0aa64d5..48d1658aa 100644 --- a/apps/api/src/app/portfolio/rules.service.ts +++ b/apps/api/src/app/portfolio/rules.service.ts @@ -9,8 +9,6 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class RulesService { - public constructor() {} - public async evaluate( aRules: Rule[], aUserSettings: UserSettings 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/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts b/apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts index cae4f22ed..83b66b370 100644 --- a/apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts +++ b/apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts @@ -19,8 +19,6 @@ import { map } from 'rxjs/operators'; export class RedactValuesInResponseInterceptor implements NestInterceptor { - public constructor() {} - public intercept( context: ExecutionContext, next: CallHandler 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/api/src/services/api/api.service.ts b/apps/api/src/services/api/api.service.ts index 5bcc6bb1d..8ff438ef1 100644 --- a/apps/api/src/services/api/api.service.ts +++ b/apps/api/src/services/api/api.service.ts @@ -4,8 +4,6 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class ApiService { - public constructor() {} - public buildFiltersFromQueryParams({ filterByAccounts, filterByAssetClasses, diff --git a/apps/api/src/services/cryptocurrency/cryptocurrency.service.ts b/apps/api/src/services/cryptocurrency/cryptocurrency.service.ts index 53feb8cc9..db5cf0876 100644 --- a/apps/api/src/services/cryptocurrency/cryptocurrency.service.ts +++ b/apps/api/src/services/cryptocurrency/cryptocurrency.service.ts @@ -7,8 +7,6 @@ const customCryptocurrencies = require('../../assets/cryptocurrencies/custom.jso export class CryptocurrencyService { private combinedCryptocurrencies: string[]; - public constructor() {} - public isCryptocurrency(aSymbol = '') { const cryptocurrencySymbol = aSymbol.substring(0, aSymbol.length - 3); return this.getCryptocurrencies().includes(cryptocurrencySymbol); diff --git a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts index 0e12b8f02..437ef4eba 100644 --- a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts @@ -37,7 +37,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { symbol: string; }): Promise> { if ( - !(response.assetClass === 'EQUITY' && response.assetSubClass === 'ETF') + !( + response.assetClass === 'EQUITY' && + ['ETF', 'MUTUALFUND'].includes(response.assetSubClass) + ) ) { return response; } diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index 78325d447..c3c948b47 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -500,6 +500,10 @@ export class EodHistoricalDataService implements DataProviderInterface { assetClass = AssetClass.EQUITY; assetSubClass = AssetSubClass.ETF; break; + case 'fund': + assetClass = AssetClass.EQUITY; + assetSubClass = AssetSubClass.MUTUALFUND; + break; } return { assetClass, assetSubClass }; diff --git a/apps/api/tsconfig.app.json b/apps/api/tsconfig.app.json index a0c17b4fa..655120714 100644 --- a/apps/api/tsconfig.app.json +++ b/apps/api/tsconfig.app.json @@ -4,7 +4,8 @@ "outDir": "../../dist/out-tsc", "types": ["node"], "emitDecoratorMetadata": true, - "target": "es2021" + "target": "es2021", + "module": "commonjs" }, "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"], "include": ["**/*.ts"] diff --git a/apps/client/src/app/app.component.html b/apps/client/src/app/app.component.html index ff21a229d..b12855488 100644 --- a/apps/client/src/app/app.component.html +++ b/apps/client/src/app/app.component.html @@ -46,7 +46,7 @@ @if (showFooter) { -