From 0f7d7447d2b901fb546f5eecc6969edde7a079ce Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 30 Oct 2024 10:59:48 +0100 Subject: [PATCH 1/4] Merge main into dockerpush --- .eslintrc.json | 10 +- .husky/pre-commit | 2 +- CHANGELOG.md | 100 + Dockerfile | 1 + README.md | 6 + .../account-balance.service.ts | 2 +- apps/api/src/app/account/account.service.ts | 4 +- .../api/src/app/account/create-account.dto.ts | 2 +- .../api/src/app/account/update-account.dto.ts | 2 +- .../src/app/admin/queue/queue.controller.ts | 4 +- apps/api/src/app/admin/queue/queue.service.ts | 4 +- apps/api/src/app/auth/auth.controller.ts | 13 +- apps/api/src/app/auth/google.strategy.ts | 8 +- .../src/app/auth/interfaces/simplewebauthn.ts | 4 +- apps/api/src/app/auth/web-auth.service.ts | 11 +- .../src/app/benchmark/benchmark.service.ts | 4 +- apps/api/src/app/health/health.controller.ts | 7 +- apps/api/src/app/order/order.service.ts | 6 +- .../calculator/portfolio-calculator.ts | 8 +- ...ulator-novn-buy-and-sell-partially.spec.ts | 56 +- .../calculator/twr/portfolio-calculator.ts | 4 +- .../src/app/portfolio/portfolio.service.ts | 56 +- apps/api/src/app/portfolio/rules.service.ts | 9 +- .../src/app/redis-cache/redis-cache.module.ts | 4 +- .../subscription/subscription.controller.ts | 4 +- .../app/subscription/subscription.service.ts | 4 +- .../src/app/user/update-user-setting.dto.ts | 14 +- apps/api/src/app/user/user.controller.ts | 2 +- apps/api/src/app/user/user.service.ts | 64 +- apps/api/src/assets/sitemap.xml | 24 + apps/api/src/helper/object.helper.ts | 2 +- .../redact-values-in-response.interceptor.ts | 2 - ...orm-data-source-in-response.interceptor.ts | 2 +- apps/api/src/models/rule.ts | 10 +- .../current-investment.ts | 16 +- .../account-cluster-risk/single-account.ts | 6 +- .../base-currency-current-investment.ts | 6 +- .../current-investment.ts | 16 +- .../developed-markets.ts | 84 + .../emerging-markets.ts | 84 + .../emergency-fund/emergency-fund-setup.ts | 6 +- .../fees/fee-ratio-initial-investment.ts | 16 +- apps/api/src/services/api/api.service.ts | 18 +- .../configuration/configuration.service.ts | 18 +- .../cryptocurrency/cryptocurrency.service.ts | 2 - .../trackinsight/trackinsight.service.ts | 7 +- .../yahoo-finance.service.spec.ts | 4 - .../yahoo-finance/yahoo-finance.service.ts | 2 - .../eod-historical-data.service.ts | 10 +- .../google-sheets/google-sheets.service.ts | 2 +- .../interfaces/environment.interface.ts | 4 + .../market-data/market-data.service.ts | 20 +- .../data-gathering.processor.ts | 14 +- .../portfolio-snapshot.module.ts | 4 +- .../portfolio-snapshot.processor.ts | 14 +- .../portfolio-snapshot.service.ts | 4 +- .../symbol-profile/symbol-profile.service.ts | 2 +- apps/api/tsconfig.app.json | 3 +- apps/client/src/app/app.component.html | 11 +- apps/client/src/app/app.component.scss | 7 + apps/client/src/app/app.component.ts | 9 +- .../access-table/access-table.component.ts | 5 +- .../accounts-table.component.ts | 8 +- .../admin-jobs/admin-jobs.component.ts | 4 +- .../admin-market-data-detail.component.ts | 11 +- .../admin-market-data.component.ts | 29 +- .../admin-market-data.service.ts | 3 +- .../asset-profile-dialog.component.ts | 6 +- .../admin-overview.component.ts | 4 +- .../admin-platform.component.ts | 6 +- .../admin-settings.component.html | 35 + .../admin-settings.component.ts | 53 +- .../admin-settings/admin-settings.module.ts | 6 + ...ghostfolio-premium-api-dialog.component.ts | 39 + .../ghostfolio-premium-api-dialog.html | 42 + .../ghostfolio-premium-api-dialog.scss | 2 + .../interfaces/interfaces.ts | 4 + .../admin-tag/admin-tag.component.ts | 6 +- .../admin-users/admin-users.component.ts | 3 +- .../asset-profile-icon.component.ts | 2 - .../benchmark-comparator.component.ts | 21 +- .../dialog-footer/dialog-footer.component.ts | 7 +- .../dialog-header/dialog-header.component.ts | 7 +- .../fear-and-greed-index.component.ts | 9 +- .../components/header/header.component.html | 3 +- .../holding-detail-dialog.component.ts | 2 +- .../home-holdings/home-holdings.component.ts | 7 +- .../home-market/home-market.component.ts | 3 - .../components/home-market/home-market.html | 10 - .../home-market/home-market.module.ts | 4 +- .../investment-chart.component.ts | 35 +- ...ogin-with-access-token-dialog.component.ts | 2 - .../portfolio-summary.component.ts | 7 +- .../interfaces/interfaces.ts | 6 +- .../rule-settings-dialog.component.ts | 14 +- .../rule-settings-dialog.html | 90 +- .../app/components/rule/rule.component.html | 2 +- .../src/app/components/rule/rule.component.ts | 19 +- .../app/components/rules/rules.component.html | 1 + .../app/components/rules/rules.component.ts | 8 +- .../interfaces/interfaces.ts | 6 +- ...scription-interstitial-dialog.component.ts | 7 +- .../subscription-interstitial-dialog.html | 152 +- ...subscription-interstitial-dialog.module.ts | 2 + .../app/components/toggle/toggle.component.ts | 9 +- .../user-account-access.component.ts | 2 +- .../user-account-membership.component.ts | 7 +- .../user-account-membership.html | 3 +- .../world-map-chart.component.ts | 7 +- apps/client/src/app/core/language.service.ts | 4 +- .../changelog/changelog-page.component.ts | 2 - .../about/license/license-page.component.ts | 2 - .../oss-friends/oss-friends-page.component.ts | 2 - .../privacy-policy-page.component.ts | 2 - .../pages/accounts/accounts-page.component.ts | 10 +- .../self-hosting-page.component.ts | 4 - .../faq/self-hosting/self-hosting-page.html | 56 +- .../src/app/pages/i18n/i18n-page.component.ts | 8 +- .../pages/markets/markets-page.component.ts | 8 +- .../activities/activities-page.component.ts | 12 +- .../allocations/allocations-page.component.ts | 6 +- .../analysis/analysis-page.component.ts | 7 +- .../portfolio/analysis/analysis-page.html | 4 - .../portfolio/fire/fire-page.component.ts | 13 + .../app/pages/portfolio/fire/fire-page.html | 24 + .../pages/pricing/pricing-page.component.ts | 3 + .../src/app/pages/pricing/pricing-page.html | 10 +- .../show-access-token-dialog.component.ts | 2 - .../resources-glossary-routing.module.ts | 18 + .../resources-glossary.component.html | 138 + .../resources-glossary.component.scss | 17 + .../glossary/resources-glossary.component.ts | 30 + .../glossary/resources-glossary.module.ts | 12 + .../guides/resources-guides-routing.module.ts | 18 + .../guides/resources-guides.component.html | 40 + .../guides/resources-guides.component.scss | 17 + .../guides/resources-guides.component.ts | 8 + .../guides/resources-guides.module.ts | 12 + .../resources-markets-routing.module.ts | 18 + .../markets/resources-markets.component.html | 64 + .../markets/resources-markets.component.scss | 17 + .../markets/resources-markets.component.ts | 8 + .../markets/resources-markets.module.ts | 11 + .../resources-overview-routing.module.ts | 17 + .../resources-overview.component.html | 16 + .../resources-overview.component.scss | 17 + .../overview/resources-overview.component.ts | 44 + .../overview/resources-overview.module.ts | 12 + .../personal-finance-tools-page.component.ts | 2 - .../resources-page-routing.module.ts | 46 +- .../resources/resources-page.component.ts | 44 +- .../app/pages/resources/resources-page.html | 288 +- .../pages/resources/resources-page.module.ts | 9 +- .../app/pages/resources/resources-page.scss | 10 - .../src/app/pipes/symbol/symbol.pipe.ts | 2 - apps/client/src/app/services/data.service.ts | 20 +- .../src/app/services/ics/ics.service.ts | 2 - .../services/impersonation-storage.service.ts | 2 - .../app/services/settings-storage.service.ts | 2 - .../src/app/services/user/user.service.ts | 8 +- apps/client/src/app/util/form.util.ts | 2 +- apps/client/src/locales/messages.ca.xlf | 534 +- apps/client/src/locales/messages.de.xlf | 534 +- apps/client/src/locales/messages.es.xlf | 534 +- apps/client/src/locales/messages.fr.xlf | 534 +- apps/client/src/locales/messages.it.xlf | 534 +- apps/client/src/locales/messages.nl.xlf | 534 +- apps/client/src/locales/messages.pl.xlf | 534 +- apps/client/src/locales/messages.pt.xlf | 534 +- apps/client/src/locales/messages.tr.xlf | 534 +- apps/client/src/locales/messages.xlf | 520 +- apps/client/src/locales/messages.zh.xlf | 534 +- apps/client/src/main.ts | 6 +- apps/client/src/styles.scss | 8 + apps/client/src/styles/theme.scss | 1 + docker/docker-compose.build.yml | 2 +- docker/docker-compose.yml | 2 +- jest.config.ts | 4 +- libs/common/src/lib/chart-helper.ts | 2 +- libs/common/src/lib/config.ts | 18 +- libs/common/src/lib/helper.ts | 2 +- libs/common/src/lib/interfaces/index.ts | 6 +- .../portfolio-report-rule.interface.ts | 14 +- .../toggle-option.interface.ts} | 4 +- .../lib/interfaces/user-settings.interface.ts | 4 +- .../x-ray-rules-settings.interface.ts} | 12 +- libs/common/src/lib/personal-finance-tools.ts | 28 + libs/common/src/lib/types/index.ts | 6 +- .../src/lib/types/subscription-offer.type.ts | 6 +- .../account-balances.component.ts | 4 +- .../activities-filter.component.ts | 2 +- .../activity-type/activity-type.component.ts | 2 - .../src/lib/assistant/assistant.component.ts | 4 +- .../lib/benchmark/benchmark.component.html | 15 + .../src/lib/benchmark/benchmark.component.ts | 11 +- .../src/lib/carousel/carousel.component.scss | 2 +- .../data-provider-credits.component.ts | 2 - .../fire-calculator.service.ts | 2 - .../holdings-table.component.ts | 5 +- libs/ui/src/lib/i18n.ts | 2 + .../lib/line-chart/line-chart.component.ts | 26 +- libs/ui/src/lib/logo/logo.component.scss | 1 + libs/ui/src/lib/logo/logo.component.ts | 2 - .../no-transactions-info.component.ts | 2 - .../portfolio-proportion-chart.component.ts | 16 +- .../premium-indicator.component.ts | 2 - .../src/lib/shared/abstract-mat-form-field.ts | 2 +- .../top-holdings/top-holdings.component.ts | 2 +- .../treemap-chart/interfaces/interfaces.ts | 5 + .../treemap-chart/treemap-chart.component.ts | 221 +- .../trend-indicator.component.ts | 2 - libs/ui/src/lib/value/value.component.ts | 6 +- nx.json | 2 +- package-lock.json | 12462 ++++++++-------- package.json | 79 +- prisma/schema.prisma | 4 +- .../ok-novn-buy-and-sell-partially.json | 8 +- tsconfig.base.json | 4 +- 218 files changed, 12414 insertions(+), 9182 deletions(-) create mode 100644 apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts create mode 100644 apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts create mode 100644 apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.component.ts create mode 100644 apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html create mode 100644 apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.scss create mode 100644 apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/interfaces/interfaces.ts create mode 100644 apps/client/src/app/pages/resources/glossary/resources-glossary-routing.module.ts create mode 100644 apps/client/src/app/pages/resources/glossary/resources-glossary.component.html create mode 100644 apps/client/src/app/pages/resources/glossary/resources-glossary.component.scss create mode 100644 apps/client/src/app/pages/resources/glossary/resources-glossary.component.ts create mode 100644 apps/client/src/app/pages/resources/glossary/resources-glossary.module.ts create mode 100644 apps/client/src/app/pages/resources/guides/resources-guides-routing.module.ts create mode 100644 apps/client/src/app/pages/resources/guides/resources-guides.component.html create mode 100644 apps/client/src/app/pages/resources/guides/resources-guides.component.scss create mode 100644 apps/client/src/app/pages/resources/guides/resources-guides.component.ts create mode 100644 apps/client/src/app/pages/resources/guides/resources-guides.module.ts create mode 100644 apps/client/src/app/pages/resources/markets/resources-markets-routing.module.ts create mode 100644 apps/client/src/app/pages/resources/markets/resources-markets.component.html create mode 100644 apps/client/src/app/pages/resources/markets/resources-markets.component.scss create mode 100644 apps/client/src/app/pages/resources/markets/resources-markets.component.ts create mode 100644 apps/client/src/app/pages/resources/markets/resources-markets.module.ts create mode 100644 apps/client/src/app/pages/resources/overview/resources-overview-routing.module.ts create mode 100644 apps/client/src/app/pages/resources/overview/resources-overview.component.html create mode 100644 apps/client/src/app/pages/resources/overview/resources-overview.component.scss create mode 100644 apps/client/src/app/pages/resources/overview/resources-overview.component.ts create mode 100644 apps/client/src/app/pages/resources/overview/resources-overview.module.ts rename libs/common/src/lib/{types/toggle-option.type.ts => interfaces/toggle-option.interface.ts} (50%) rename libs/common/src/lib/{types/x-ray-rules-settings.type.ts => interfaces/x-ray-rules-settings.interface.ts} (58%) create mode 100644 libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts diff --git a/.eslintrc.json b/.eslintrc.json index d3f7edea9..75e362465 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -39,6 +39,7 @@ "plugin:@typescript-eslint/stylistic-type-checked" ], "rules": { + "@typescript-eslint/consistent-indexed-object-style": "off", "@typescript-eslint/dot-notation": "off", "@typescript-eslint/explicit-member-accessibility": [ "off", @@ -142,14 +143,7 @@ // The following rules are part of @typescript-eslint/stylistic-type-checked // and can be remove once solved - "@typescript-eslint/consistent-type-definitions": "warn", - "@typescript-eslint/prefer-function-type": "warn", - "@typescript-eslint/no-empty-function": "warn", - "@typescript-eslint/prefer-nullish-coalescing": "warn", // TODO: Requires strictNullChecks: true - "@typescript-eslint/consistent-type-assertions": "warn", - "@typescript-eslint/prefer-optional-chain": "warn", - "@typescript-eslint/consistent-indexed-object-style": "warn", - "@typescript-eslint/consistent-generic-constructors": "warn" + "@typescript-eslint/prefer-nullish-coalescing": "warn" // TODO: Requires strictNullChecks: true } } ], diff --git a/.husky/pre-commit b/.husky/pre-commit index 4bf77522a..b82bf440f 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,6 +1,6 @@ # Run linting and stop the commit process if any errors are found # --quiet suppresses warnings (temporary until all warnings are fixed) -npm run lint --quiet || exit 1 +npm run affected:lint --base=main --head=HEAD --parallel=2 --quiet || exit 1 # Check formatting on modified and uncommitted files, stop the commit if issues are found npm run format:check --uncommitted || exit 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cbac09ae..5bdd653a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,19 +7,119 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Restructured the resources page +- Renamed the static portfolio analysis rule from _Allocation Cluster Risk_ to _Economic Market Cluster Risk_ (Developed Markets and Emerging Markets) +- Improved the language localization for German (`de`) +- Switched the `consistent-generic-constructors` rule from `warn` to `error` in the `eslint` configuration +- Switched the `consistent-indexed-object-style` rule from `warn` to `off` in the `eslint` configuration +- Switched the `consistent-type-assertions` rule from `warn` to `error` in the `eslint` configuration +- Switched the `prefer-optional-chain` rule from `warn` to `error` in the `eslint` configuration +- Upgraded `Nx` from version `20.0.3` to `20.0.6` + +## 2.119.0 - 2024-10-26 + +### Changed + +- Switched the `consistent-type-definitions` rule from `warn` to `error` in the `eslint` configuration +- Switched the `no-empty-function` rule from `warn` to `error` in the `eslint` configuration +- Switched the `prefer-function-type` rule from `warn` to `error` in the `eslint` configuration +- Upgraded `prisma` from version `5.20.0` to `5.21.1` + +### Fixed + +- Fixed an issue with the X-axis scale of the dividend timeline on the analysis page +- Fixed an issue with the X-axis scale of the investment timeline on the analysis page +- Fixed an issue with the X-axis scale of the portfolio evolution chart on the analysis page +- Fixed an issue in the calculation of the static portfolio analysis rule: _Allocation Cluster Risk_ (Developed Markets) +- Fixed an issue in the calculation of the static portfolio analysis rule: _Allocation Cluster Risk_ (Emerging Markets) + +## 2.118.0 - 2024-10-23 + +### 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 + +### Changed + +- Improved the font colors of the chart of the holdings tab on the home page (experimental) +- Optimized the dialog sizes for mobile (full screen) +- Optimized the git-hook via `husky` to lint only affected projects before a commit +- 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 + +- Fixed an issue in the carousel component for the testimonial section on the landing page + +## 2.116.0 - 2024-10-17 + +### Added + +- Extended the content of the _Self-Hosting_ section by the benchmarks concept for _Compare with..._ on the Frequently Asked Questions (FAQ) page +- Extended the content of the _Self-Hosting_ section by the benchmarks concept for _Markets_ on the Frequently Asked Questions (FAQ) page +- Set the permissions (`chmod 0700`) on `entrypoint.sh` in the `Dockerfile` + +### Changed + +- Improved the empty state in the benchmarks of the markets overview +- Disabled the text hover effect in the chart of the holdings tab on the home page (experimental) +- Improved the usability to customize the rule thresholds in the _X-ray_ section by introducing units (experimental) +- Switched to adjusted market prices (splits and dividends) in the get historical functionality of the _EOD Historical Data_ service +- Improved the language localization for German (`de`) + +### Fixed + +- Fixed the usage of the environment variable `PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY` + +## 2.115.0 - 2024-10-14 + ### Added - Added the name to the tooltip of the chart of the holdings tab on the home page (experimental) ### Changed +- Improved the backgrounds of the chart of the holdings tab on the home page (experimental) +- Improved the labels of the chart of the holdings tab on the home page (experimental) +- Improved the usability to customize the rule thresholds in the _X-ray_ section by introducing sliders (experimental) +- Refactored the rule thresholds in the _X-ray_ section (experimental) - Exposed the timeout of the portfolio snapshot computation as an environment variable (`PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT`) +- Harmonized the processor concurrency environment variables - Improved the portfolio unit tests to work with exported activity files +- Enabled the `noUnusedLocals` compiler option in the `tsconfig` +- Enabled the `noUnusedParameters` compiler option in the `tsconfig` ### Fixed - Considered the language of the user settings on login with _Security Token_ +### Todo + +- Rename the environment variable from `PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE` to `PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY` +- Rename the environment variable from `PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA` to `PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY` +- Rename the environment variable from `PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT` to `PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY` + ## 2.114.0 - 2024-10-10 ### Added diff --git a/Dockerfile b/Dockerfile index e6c38f273..0e5c0d275 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,6 +61,7 @@ RUN apt-get update && apt-get install -y --no-install-suggests \ COPY --chown=node:node --from=builder /ghostfolio/dist/apps /ghostfolio/apps COPY --chown=node:node ./docker/entrypoint.sh /ghostfolio/entrypoint.sh +RUN chmod 0700 /ghostfolio/entrypoint.sh WORKDIR /ghostfolio/apps/api EXPOSE ${PORT:-3333} USER node diff --git a/README.md b/README.md index 989cc37ea..dca360c39 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,12 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/where.userId + userId: where.userId as string }) ); diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 22ea82016..7b9b09c0c 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -211,8 +211,8 @@ export class AccountService { const { data, where } = params; await this.accountBalanceService.createOrUpdateAccountBalance({ - accountId: data.id, - balance: data.balance, + accountId: data.id as string, + balance: data.balance as number, date: format(new Date(), DATE_FORMAT), userId: aUserId }); diff --git a/apps/api/src/app/account/create-account.dto.ts b/apps/api/src/app/account/create-account.dto.ts index f3c88316f..b331d4ec7 100644 --- a/apps/api/src/app/account/create-account.dto.ts +++ b/apps/api/src/app/account/create-account.dto.ts @@ -36,6 +36,6 @@ export class CreateAccountDto { name: string; @IsString() - @ValidateIf((object, value) => value !== null) + @ValidateIf((_object, value) => value !== null) platformId: string | null; } diff --git a/apps/api/src/app/account/update-account.dto.ts b/apps/api/src/app/account/update-account.dto.ts index 6b87af71b..3a721d873 100644 --- a/apps/api/src/app/account/update-account.dto.ts +++ b/apps/api/src/app/account/update-account.dto.ts @@ -35,6 +35,6 @@ export class UpdateAccountDto { name: string; @IsString() - @ValidateIf((object, value) => value !== null) + @ValidateIf((_object, value) => value !== null) platformId: string | null; } diff --git a/apps/api/src/app/admin/queue/queue.controller.ts b/apps/api/src/app/admin/queue/queue.controller.ts index 978cb9721..060abd247 100644 --- a/apps/api/src/app/admin/queue/queue.controller.ts +++ b/apps/api/src/app/admin/queue/queue.controller.ts @@ -26,7 +26,7 @@ export class QueueController { public async deleteJobs( @Query('status') filterByStatus?: string ): Promise { - const status = filterByStatus?.split(',') ?? undefined; + const status = (filterByStatus?.split(',') as JobStatus[]) ?? undefined; return this.queueService.deleteJobs({ status }); } @@ -36,7 +36,7 @@ export class QueueController { public async getJobs( @Query('status') filterByStatus?: string ): Promise { - const status = filterByStatus?.split(',') ?? undefined; + const status = (filterByStatus?.split(',') as JobStatus[]) ?? undefined; return this.queueService.getJobs({ status }); } diff --git a/apps/api/src/app/admin/queue/queue.service.ts b/apps/api/src/app/admin/queue/queue.service.ts index 7e4f0adb7..b0058e81f 100644 --- a/apps/api/src/app/admin/queue/queue.service.ts +++ b/apps/api/src/app/admin/queue/queue.service.ts @@ -1,6 +1,6 @@ import { DATA_GATHERING_QUEUE, - PORTFOLIO_SNAPSHOT_QUEUE, + PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE, QUEUE_JOB_STATUS_LIST } from '@ghostfolio/common/config'; import { AdminJobs } from '@ghostfolio/common/interfaces'; @@ -14,7 +14,7 @@ export class QueueService { public constructor( @InjectQueue(DATA_GATHERING_QUEUE) private readonly dataGatheringQueue: Queue, - @InjectQueue(PORTFOLIO_SNAPSHOT_QUEUE) + @InjectQueue(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE) private readonly portfolioSnapshotQueue: Queue ) {} diff --git a/apps/api/src/app/auth/auth.controller.ts b/apps/api/src/app/auth/auth.controller.ts index c81c7e224..a91525269 100644 --- a/apps/api/src/app/auth/auth.controller.ts +++ b/apps/api/src/app/auth/auth.controller.ts @@ -14,12 +14,12 @@ import { Req, Res, UseGuards, - VERSION_NEUTRAL, - Version + Version, + VERSION_NEUTRAL } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { Request, Response } from 'express'; -import { StatusCodes, getReasonPhrase } from 'http-status-codes'; +import { getReasonPhrase, StatusCodes } from 'http-status-codes'; import { AuthService } from './auth.service'; import { @@ -85,7 +85,7 @@ export class AuthController { @Res() response: Response ) { // Handles the Google OAuth2 callback - const jwt: string = (request.user).jwt; + const jwt: string = (request.user as any).jwt; if (jwt) { response.redirect( @@ -130,10 +130,7 @@ export class AuthController { public async verifyAttestation( @Body() body: { deviceName: string; credential: AttestationCredentialJSON } ) { - return this.webAuthService.verifyAttestation( - body.deviceName, - body.credential - ); + return this.webAuthService.verifyAttestation(body.credential); } @Post('webauthn/generate-assertion-options') diff --git a/apps/api/src/app/auth/google.strategy.ts b/apps/api/src/app/auth/google.strategy.ts index ea6772680..02f82a7a8 100644 --- a/apps/api/src/app/auth/google.strategy.ts +++ b/apps/api/src/app/auth/google.strategy.ts @@ -11,7 +11,7 @@ import { AuthService } from './auth.service'; export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { public constructor( private readonly authService: AuthService, - private readonly configurationService: ConfigurationService + configurationService: ConfigurationService ) { super({ callbackURL: `${configurationService.get( @@ -25,9 +25,9 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') { } public async validate( - request: any, - token: string, - refreshToken: string, + _request: any, + _token: string, + _refreshToken: string, profile: Profile, done: Function ) { diff --git a/apps/api/src/app/auth/interfaces/simplewebauthn.ts b/apps/api/src/app/auth/interfaces/simplewebauthn.ts index 4b9058e2f..ef0a14ffa 100644 --- a/apps/api/src/app/auth/interfaces/simplewebauthn.ts +++ b/apps/api/src/app/auth/interfaces/simplewebauthn.ts @@ -198,12 +198,12 @@ export interface AuthenticatorAssertionResponseJSON /** * A WebAuthn-compatible device and the information needed to verify assertions by it */ -export declare type AuthenticatorDevice = { +export declare interface AuthenticatorDevice { credentialPublicKey: Buffer; credentialID: Buffer; counter: number; transports?: AuthenticatorTransport[]; -}; +} /** * An attempt to communicate that this isn't just any string, but a Base64URL-encoded string */ diff --git a/apps/api/src/app/auth/web-auth.service.ts b/apps/api/src/app/auth/web-auth.service.ts index 961bbe9a7..2f8dd1018 100644 --- a/apps/api/src/app/auth/web-auth.service.ts +++ b/apps/api/src/app/auth/web-auth.service.ts @@ -13,16 +13,16 @@ import { import { REQUEST } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { + generateAuthenticationOptions, GenerateAuthenticationOptionsOpts, + generateRegistrationOptions, GenerateRegistrationOptionsOpts, VerifiedAuthenticationResponse, VerifiedRegistrationResponse, - VerifyAuthenticationResponseOpts, - VerifyRegistrationResponseOpts, - generateAuthenticationOptions, - generateRegistrationOptions, verifyAuthenticationResponse, - verifyRegistrationResponse + VerifyAuthenticationResponseOpts, + verifyRegistrationResponse, + VerifyRegistrationResponseOpts } from '@simplewebauthn/server'; import { @@ -80,7 +80,6 @@ export class WebAuthService { } public async verifyAttestation( - deviceName: string, credential: AttestationCredentialJSON ): Promise { const user = this.request.user; diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts index 34f101a8e..36f196842 100644 --- a/apps/api/src/app/benchmark/benchmark.service.ts +++ b/apps/api/src/app/benchmark/benchmark.service.ts @@ -442,10 +442,10 @@ export class BenchmarkService { await this.redisCacheService.set( this.CACHE_KEY_BENCHMARKS, - JSON.stringify({ + JSON.stringify({ benchmarks, expiration: expiration.getTime() - }), + } as BenchmarkValue), CACHE_TTL_INFINITE ); } diff --git a/apps/api/src/app/health/health.controller.ts b/apps/api/src/app/health/health.controller.ts index 31bdb2e8f..62ee20419 100644 --- a/apps/api/src/app/health/health.controller.ts +++ b/apps/api/src/app/health/health.controller.ts @@ -3,7 +3,9 @@ import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interce import { Controller, Get, + HttpCode, HttpException, + HttpStatus, Param, UseInterceptors } from '@nestjs/common'; @@ -17,7 +19,10 @@ export class HealthController { public constructor(private readonly healthService: HealthService) {} @Get() - public async getHealth() {} + @HttpCode(HttpStatus.OK) + public getHealth() { + return { status: getReasonPhrase(StatusCodes.OK) }; + } @Get('data-enhancer/:name') public async getHealthOfDataEnhancer(@Param('name') name: string) { diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 30a0ef07d..18f996a52 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -418,7 +418,7 @@ export class OrderService { where.SymbolProfile, { AND: [ - { dataSource: filterByDataSource }, + { dataSource: filterByDataSource as DataSource }, { symbol: filterBySymbol } ] } @@ -427,7 +427,7 @@ export class OrderService { } else { where.SymbolProfile = { AND: [ - { dataSource: filterByDataSource }, + { dataSource: filterByDataSource as DataSource }, { symbol: filterBySymbol } ] }; @@ -671,7 +671,7 @@ export class OrderService { { dataSource: data.SymbolProfile.connect.dataSource_symbol.dataSource, - date: data.date, + date: data.date as Date, symbol: data.SymbolProfile.connect.dataSource_symbol.symbol } ], diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 84bb3bcb5..30f6ec264 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -15,8 +15,8 @@ import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper' import { PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME, PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS, - PORTFOLIO_SNAPSHOT_QUEUE_PRIORITY_HIGH, - PORTFOLIO_SNAPSHOT_QUEUE_PRIORITY_LOW + PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_HIGH, + PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW } from '@ghostfolio/common/config'; import { DATE_FORMAT, @@ -1091,7 +1091,7 @@ export abstract class PortfolioCalculator { opts: { ...PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS, jobId, - priority: PORTFOLIO_SNAPSHOT_QUEUE_PRIORITY_LOW + priority: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW } }); } @@ -1107,7 +1107,7 @@ export abstract class PortfolioCalculator { opts: { ...PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS, jobId, - priority: PORTFOLIO_SNAPSHOT_QUEUE_PRIORITY_HIGH + priority: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_HIGH } }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index 4da97674b..888d1e968 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -1,6 +1,8 @@ +import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, + loadActivityExportFile, symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; @@ -20,6 +22,7 @@ import { parseDate } from '@ghostfolio/common/helper'; import { Big } from 'big.js'; import { last } from 'lodash'; +import { join } from 'path'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { @@ -52,6 +55,8 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { + let activityDtos: CreateOrderDto[]; + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; @@ -59,6 +64,15 @@ describe('PortfolioCalculator', () => { let portfolioSnapshotService: PortfolioSnapshotService; let redisCacheService: RedisCacheService; + beforeAll(() => { + activityDtos = loadActivityExportFile( + join( + __dirname, + '../../../../../../../test/import/ok-novn-buy-and-sell-partially.json' + ) + ); + }); + beforeEach(() => { configurationService = new ConfigurationService(); @@ -89,38 +103,18 @@ describe('PortfolioCalculator', () => { it.only('with NOVN.SW buy and sell partially', async () => { jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); - const activities: Activity[] = [ - { - ...activityDummyData, - date: new Date('2022-03-07'), - fee: 1.3, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Novartis AG', - symbol: 'NOVN.SW' - }, - type: 'BUY', - unitPrice: 75.8 - }, - { - ...activityDummyData, - date: new Date('2022-04-08'), - fee: 2.95, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Novartis AG', - symbol: 'NOVN.SW' - }, - type: 'SELL', - unitPrice: 85.73 + const activities: Activity[] = activityDtos.map((activity) => ({ + ...activityDummyData, + ...activity, + date: parseDate(activity.date), + SymbolProfile: { + ...symbolProfileDummyData, + currency: activity.currency, + dataSource: activity.dataSource, + name: 'Novartis AG', + symbol: activity.symbol } - ]; + })); const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index e2a523739..3474a4e7e 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -810,7 +810,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { [key: DateRange]: Big; } = {}; - for (const dateRange of [ + for (const dateRange of [ '1d', '1y', '5y', @@ -826,7 +826,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { // .map((date) => { // return format(date, 'yyyy'); // }) - ]) { + ] as DateRange[]) { const dateInterval = getIntervalFromDateRange(dateRange); const endDate = dateInterval.endDate; let startDate = dateInterval.startDate; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index d05ac4033..9eaf6760e 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -10,6 +10,8 @@ import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rule import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; 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 { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/developed-markets'; +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 { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; @@ -139,7 +141,7 @@ export class PortfolioService { some: { SymbolProfile: { AND: [ - { dataSource: filterByDataSource }, + { dataSource: filterByDataSource as DataSource }, { symbol: filterBySymbol } ] } @@ -1256,15 +1258,21 @@ export class PortfolioService { @LogPerformance public async getReport(impersonationId: string): Promise { const userId = await this.getUserId(impersonationId, this.request.user.id); - const userSettings = this.request.user.Settings.settings; + const userSettings = this.request.user.Settings.settings as UserSettings; - const { accounts, holdings, summary } = await this.getDetails({ + const { accounts, holdings, markets, summary } = await this.getDetails({ impersonationId, userId, withMarkets: true, withSummary: true }); + const marketsTotalInBaseCurrency = getSum( + Object.values(markets).map(({ valueInBaseCurrency }) => { + return new Big(valueInBaseCurrency); + }) + ).toNumber(); + return { rules: { accountClusterRisk: @@ -1283,6 +1291,24 @@ export class PortfolioService { userSettings ) : undefined, + economicMarketClusterRisk: + summary.ordersCount > 0 + ? await this.rulesService.evaluate( + [ + new EconomicMarketClusterRiskDevelopedMarkets( + this.exchangeRateDataService, + marketsTotalInBaseCurrency, + markets.developedMarkets.valueInBaseCurrency + ), + new EconomicMarketClusterRiskEmergingMarkets( + this.exchangeRateDataService, + marketsTotalInBaseCurrency, + markets.emergingMarkets.valueInBaseCurrency + ) + ], + userSettings + ) + : undefined, currencyClusterRisk: summary.ordersCount > 0 ? await this.rulesService.evaluate( @@ -1340,9 +1366,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']; } { @@ -1438,20 +1462,20 @@ export class PortfolioService { } } - const marketsTotal = - markets.developedMarkets.valueInBaseCurrency + - markets.emergingMarkets.valueInBaseCurrency + - markets.otherMarkets.valueInBaseCurrency + - markets[UNKNOWN_KEY].valueInBaseCurrency; + const marketsTotalInBaseCurrency = getSum( + Object.values(markets).map(({ valueInBaseCurrency }) => { + return new Big(valueInBaseCurrency); + }) + ).toNumber(); markets.developedMarkets.valueInPercentage = - markets.developedMarkets.valueInBaseCurrency / marketsTotal; + markets.developedMarkets.valueInBaseCurrency / marketsTotalInBaseCurrency; markets.emergingMarkets.valueInPercentage = - markets.emergingMarkets.valueInBaseCurrency / marketsTotal; + markets.emergingMarkets.valueInBaseCurrency / marketsTotalInBaseCurrency; markets.otherMarkets.valueInPercentage = - markets.otherMarkets.valueInBaseCurrency / marketsTotal; + markets.otherMarkets.valueInBaseCurrency / marketsTotalInBaseCurrency; markets[UNKNOWN_KEY].valueInPercentage = - markets[UNKNOWN_KEY].valueInBaseCurrency / marketsTotal; + markets[UNKNOWN_KEY].valueInBaseCurrency / marketsTotalInBaseCurrency; const marketsAdvancedTotal = marketsAdvanced.asiaPacific.valueInBaseCurrency + @@ -2012,7 +2036,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 fd9d794b2..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 @@ -24,13 +22,10 @@ export class RulesService { return { evaluation, value, + configuration: rule.getConfiguration(), isActive: true, key: rule.getKey(), - name: rule.getName(), - settings: { - thresholdMax: settings['thresholdMax'], - thresholdMin: settings['thresholdMin'] - } + name: rule.getName() }; } else { return { diff --git a/apps/api/src/app/redis-cache/redis-cache.module.ts b/apps/api/src/app/redis-cache/redis-cache.module.ts index a507479b9..5411309bd 100644 --- a/apps/api/src/app/redis-cache/redis-cache.module.ts +++ b/apps/api/src/app/redis-cache/redis-cache.module.ts @@ -19,11 +19,11 @@ import { RedisCacheService } from './redis-cache.service'; configurationService.get('REDIS_PASSWORD') ); - return { + return { store: redisStore, ttl: configurationService.get('CACHE_TTL'), url: `redis://${redisPassword ? `:${redisPassword}` : ''}@${configurationService.get('REDIS_HOST')}:${configurationService.get('REDIS_PORT')}/${configurationService.get('REDIS_DB')}` - }; + } as RedisClientOptions; } }), ConfigurationModule diff --git a/apps/api/src/app/subscription/subscription.controller.ts b/apps/api/src/app/subscription/subscription.controller.ts index f4ca6d427..f37543fdf 100644 --- a/apps/api/src/app/subscription/subscription.controller.ts +++ b/apps/api/src/app/subscription/subscription.controller.ts @@ -95,7 +95,7 @@ export class SubscriptionController { @Res() response: Response ) { const userId = await this.subscriptionService.createSubscriptionViaStripe( - request.query.checkoutSessionId + request.query.checkoutSessionId as string ); Logger.log( @@ -113,7 +113,7 @@ export class SubscriptionController { @Post('stripe/checkout-session') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async createCheckoutSession( - @Body() { couponId, priceId }: { couponId: string; priceId: string } + @Body() { couponId, priceId }: { couponId?: string; priceId: string } ) { try { return this.subscriptionService.createCheckoutSession({ diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index 545450669..7c1df023c 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -124,7 +124,9 @@ export class SubscriptionService { let offer: SubscriptionOffer = price ? 'renewal' : 'default'; if (isBefore(createdAt, parseDate('2023-01-01'))) { - offer = 'renewal-early-bird'; + offer = 'renewal-early-bird-2023'; + } else if (isBefore(createdAt, parseDate('2024-01-01'))) { + offer = 'renewal-early-bird-2024'; } return { diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts index d73fa47eb..317b4d689 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -1,10 +1,10 @@ import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code'; +import { XRayRulesSettings } from '@ghostfolio/common/interfaces'; import type { ColorScheme, DateRange, HoldingsViewMode, - ViewMode, - XRayRulesSettings + ViewMode } from '@ghostfolio/common/types'; import { @@ -31,11 +31,11 @@ export class UpdateUserSettingDto { @IsOptional() benchmark?: string; - @IsIn(['DARK', 'LIGHT']) + @IsIn(['DARK', 'LIGHT'] as ColorScheme[]) @IsOptional() colorScheme?: ColorScheme; - @IsIn([ + @IsIn([ '1d', '1w', '1m', @@ -51,7 +51,7 @@ export class UpdateUserSettingDto { return format(date, 'yyyy'); } ) - ]) + ] as DateRange[]) @IsOptional() dateRange?: DateRange; @@ -71,7 +71,7 @@ export class UpdateUserSettingDto { @IsOptional() 'filters.tags'?: string[]; - @IsIn(['CHART', 'TABLE']) + @IsIn(['CHART', 'TABLE'] as HoldingsViewMode[]) @IsOptional() holdingsViewMode?: HoldingsViewMode; @@ -103,7 +103,7 @@ export class UpdateUserSettingDto { @IsOptional() savingsRate?: number; - @IsIn(['DEFAULT', 'ZEN']) + @IsIn(['DEFAULT', 'ZEN'] as ViewMode[]) @IsOptional() viewMode?: ViewMode; diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index 6b6b96fce..5c6688bd7 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -148,7 +148,7 @@ export class UserController { const userSettings: UserSettings = merge( {}, - this.request.user.Settings.settings, + this.request.user.Settings.settings as UserSettings, data ); diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 0f76b9540..288e2aba2 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -2,6 +2,14 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; 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 { 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 { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/developed-markets'; +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 { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; @@ -108,8 +116,8 @@ export class UserService { accounts: Account, dateOfFirstActivity: firstActivity?.date ?? new Date(), settings: { - ...(Settings.settings), - locale: (Settings.settings)?.locale ?? aLocale + ...(Settings.settings as UserSettings), + locale: (Settings.settings as UserSettings)?.locale ?? aLocale } }; } @@ -200,17 +208,47 @@ export class UserService { (user.Settings.settings as UserSettings).viewMode = 'DEFAULT'; } - // Set default values for X-ray rules - if (!(user.Settings.settings as UserSettings).xRayRules) { - (user.Settings.settings as UserSettings).xRayRules = { - AccountClusterRiskCurrentInvestment: { isActive: true }, - AccountClusterRiskSingleAccount: { isActive: true }, - CurrencyClusterRiskBaseCurrencyCurrentInvestment: { isActive: true }, - CurrencyClusterRiskCurrentInvestment: { isActive: true }, - EmergencyFundSetup: { isActive: true }, - FeeRatioInitialInvestment: { isActive: true } - }; - } + (user.Settings.settings as UserSettings).xRayRules = { + AccountClusterRiskCurrentInvestment: + new AccountClusterRiskCurrentInvestment(undefined, {}).getSettings( + user.Settings.settings + ), + AccountClusterRiskSingleAccount: new AccountClusterRiskSingleAccount( + undefined, + {} + ).getSettings(user.Settings.settings), + EconomicMarketClusterRiskDevelopedMarkets: + new EconomicMarketClusterRiskDevelopedMarkets( + undefined, + undefined, + undefined + ).getSettings(user.Settings.settings), + EconomicMarketClusterRiskEmergingMarkets: + new EconomicMarketClusterRiskEmergingMarkets( + undefined, + undefined, + undefined + ).getSettings(user.Settings.settings), + CurrencyClusterRiskBaseCurrencyCurrentInvestment: + new CurrencyClusterRiskBaseCurrencyCurrentInvestment( + undefined, + undefined + ).getSettings(user.Settings.settings), + CurrencyClusterRiskCurrentInvestment: + new CurrencyClusterRiskCurrentInvestment( + undefined, + undefined + ).getSettings(user.Settings.settings), + EmergencyFundSetup: new EmergencyFundSetup( + undefined, + undefined + ).getSettings(user.Settings.settings), + FeeRatioInitialInvestment: new FeeRatioInitialInvestment( + undefined, + undefined, + undefined + ).getSettings(user.Settings.settings) + }; let currentPermissions = getPermissions(user.role); diff --git a/apps/api/src/assets/sitemap.xml b/apps/api/src/assets/sitemap.xml index 93d6b38d3..3a0f44ffd 100644 --- a/apps/api/src/assets/sitemap.xml +++ b/apps/api/src/assets/sitemap.xml @@ -56,10 +56,22 @@ https://ghostfol.io/de/ressourcen ${currentDate}T00:00:00+00:00 + + https://ghostfol.io/de/ressourcen/lexikon + ${currentDate}T00:00:00+00:00 + + + https://ghostfol.io/de/ressourcen/maerkte + ${currentDate}T00:00:00+00:00 + https://ghostfol.io/de/ressourcen/personal-finance-tools ${currentDate}T00:00:00+00:00 + + https://ghostfol.io/de/ressourcen/ratgeber + ${currentDate}T00:00:00+00:00 + https://ghostfol.io/de/ueber-uns ${currentDate}T00:00:00+00:00 @@ -214,6 +226,18 @@ https://ghostfol.io/en/resources ${currentDate}T00:00:00+00:00 + + https://ghostfol.io/en/resources/glossary + ${currentDate}T00:00:00+00:00 + + + https://ghostfol.io/en/resources/guides + ${currentDate}T00:00:00+00:00 + + + https://ghostfol.io/en/resources/markets + ${currentDate}T00:00:00+00:00 + https://ghostfol.io/en/resources/personal-finance-tools ${currentDate}T00:00:00+00:00 diff --git a/apps/api/src/helper/object.helper.ts b/apps/api/src/helper/object.helper.ts index c6d825598..a5854e9d9 100644 --- a/apps/api/src/helper/object.helper.ts +++ b/apps/api/src/helper/object.helper.ts @@ -40,7 +40,7 @@ export function redactAttributes({ object: any; options: { attribute: string; valueMap: { [key: string]: any } }[]; }): any { - if (!object || !options || !options.length) { + if (!object || !options?.length) { return object; } 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/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts index aff42f002..f5034927c 100644 --- a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts @@ -21,7 +21,7 @@ export class TransformDataSourceInResponseInterceptor ) {} public intercept( - context: ExecutionContext, + _context: ExecutionContext, next: CallHandler ): Observable { return next.handle().pipe( diff --git a/apps/api/src/models/rule.ts b/apps/api/src/models/rule.ts index a1e0d9bee..187527fbb 100644 --- a/apps/api/src/models/rule.ts +++ b/apps/api/src/models/rule.ts @@ -1,7 +1,11 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { groupBy } from '@ghostfolio/common/helper'; -import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces'; +import { + PortfolioPosition, + PortfolioReportRule, + UserSettings +} from '@ghostfolio/common/interfaces'; import { Big } from 'big.js'; @@ -65,5 +69,9 @@ export abstract class Rule implements RuleInterface { public abstract evaluate(aRuleSettings: T): EvaluationResult; + public abstract getConfiguration(): Partial< + PortfolioReportRule['configuration'] + >; + public abstract getSettings(aUserSettings: UserSettings): T; } diff --git a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts index 13680270e..564af935d 100644 --- a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts +++ b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts @@ -76,11 +76,23 @@ export class AccountClusterRiskCurrentInvestment extends Rule { }; } + public getConfiguration() { + return { + threshold: { + max: 1, + min: 0, + step: 0.01, + unit: '%' + }, + thresholdMax: true + }; + } + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { return { baseCurrency, - isActive: xRayRules[this.getKey()].isActive, - thresholdMax: xRayRules[this.getKey()]?.thresholdMax ?? 0.5 + isActive: xRayRules?.[this.getKey()].isActive ?? true, + thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5 }; } } diff --git a/apps/api/src/models/rules/account-cluster-risk/single-account.ts b/apps/api/src/models/rules/account-cluster-risk/single-account.ts index feaaf4e38..ef549e579 100644 --- a/apps/api/src/models/rules/account-cluster-risk/single-account.ts +++ b/apps/api/src/models/rules/account-cluster-risk/single-account.ts @@ -34,9 +34,13 @@ export class AccountClusterRiskSingleAccount extends Rule { }; } + public getConfiguration() { + return undefined; + } + public getSettings({ xRayRules }: UserSettings): RuleSettings { return { - isActive: xRayRules[this.getKey()].isActive + isActive: xRayRules?.[this.getKey()].isActive ?? true }; } } diff --git a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts index 39ee8b88d..573795799 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts @@ -61,10 +61,14 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { }; } + public getConfiguration() { + return { + threshold: { + max: 1, + min: 0, + step: 0.01, + unit: '%' + }, + thresholdMax: true + }; + } + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { return { baseCurrency, - isActive: xRayRules[this.getKey()].isActive, - thresholdMax: xRayRules[this.getKey()]?.thresholdMax ?? 0.5 + isActive: xRayRules?.[this.getKey()].isActive ?? true, + thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5 }; } } diff --git a/apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts b/apps/api/src/models/rules/economic-market-cluster-risk/developed-markets.ts new file mode 100644 index 000000000..15e113927 --- /dev/null +++ b/apps/api/src/models/rules/economic-market-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 EconomicMarketClusterRiskDevelopedMarkets extends Rule { + private currentValueInBaseCurrency: number; + private developedMarketsValueInBaseCurrency: number; + + public constructor( + protected exchangeRateDataService: ExchangeRateDataService, + currentValueInBaseCurrency: number, + developedMarketsValueInBaseCurrency: number + ) { + super(exchangeRateDataService, { + key: EconomicMarketClusterRiskDevelopedMarkets.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/apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts b/apps/api/src/models/rules/economic-market-cluster-risk/emerging-markets.ts new file mode 100644 index 000000000..8fccdf1d8 --- /dev/null +++ b/apps/api/src/models/rules/economic-market-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 EconomicMarketClusterRiskEmergingMarkets extends Rule { + private currentValueInBaseCurrency: number; + private emergingMarketsValueInBaseCurrency: number; + + public constructor( + protected exchangeRateDataService: ExchangeRateDataService, + currentValueInBaseCurrency: number, + emergingMarketsValueInBaseCurrency: number + ) { + super(exchangeRateDataService, { + key: EconomicMarketClusterRiskEmergingMarkets.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/models/rules/emergency-fund/emergency-fund-setup.ts b/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts index 819b8bd7b..d13f2ffc5 100644 --- a/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts +++ b/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts @@ -32,10 +32,14 @@ export class EmergencyFundSetup extends Rule { }; } + public getConfiguration() { + return undefined; + } + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { return { baseCurrency, - isActive: xRayRules[this.getKey()].isActive + isActive: xRayRules?.[this.getKey()].isActive ?? true }; } } 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 9b1961ed6..fa9d7e7bc 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 @@ -43,11 +43,23 @@ export class FeeRatioInitialInvestment extends Rule { }; } + public getConfiguration() { + return { + threshold: { + max: 0.1, + min: 0, + step: 0.0025, + unit: '%' + }, + thresholdMax: true + }; + } + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { return { baseCurrency, - isActive: xRayRules[this.getKey()].isActive, - thresholdMax: xRayRules[this.getKey()]?.thresholdMax ?? 0.01 + isActive: xRayRules?.[this.getKey()].isActive ?? true, + thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.01 }; } } diff --git a/apps/api/src/services/api/api.service.ts b/apps/api/src/services/api/api.service.ts index 5bcc6bb1d..052119246 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, @@ -36,28 +34,28 @@ export class ApiService { const filters = [ ...accountIds.map((accountId) => { - return { + return { id: accountId, type: 'ACCOUNT' - }; + } as Filter; }), ...assetClasses.map((assetClass) => { - return { + return { id: assetClass, type: 'ASSET_CLASS' - }; + } as Filter; }), ...assetSubClasses.map((assetClass) => { - return { + return { id: assetClass, type: 'ASSET_SUB_CLASS' - }; + } as Filter; }), ...tagIds.map((tagId) => { - return { + return { id: tagId, type: 'TAG' - }; + } as Filter; }) ]; diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index dafd4803c..10810deb5 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -1,9 +1,9 @@ import { Environment } from '@ghostfolio/api/services/interfaces/environment.interface'; import { CACHE_TTL_NO_CACHE, - DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE, - DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA, - DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT, + DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY, + DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY, + DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY, DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT, DEFAULT_ROOT_URL } from '@ghostfolio/common/config'; @@ -51,14 +51,14 @@ export class ConfigurationService { MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }), MAX_CHART_ITEMS: num({ default: 365 }), PORT: port({ default: 3333 }), - PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE: num({ - default: DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE + PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: num({ + default: DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY }), - PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA: num({ - default: DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA + PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY: num({ + default: DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY }), - PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT: num({ - default: DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT + PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY: num({ + default: DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY }), PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT: num({ default: DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT 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 f5144da06..4e29b349e 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 @@ -36,7 +36,12 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { response: Partial; symbol: string; }): Promise> { - if (!(response.assetSubClass === 'ETF')) { + if ( + !( + response.assetClass === 'EQUITY' && + ['ETF', 'MUTUALFUND'].includes(response.assetSubClass) + ) + ) { return response; } diff --git a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.spec.ts b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.spec.ts index 951a623d0..8a8ab1f08 100644 --- a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.spec.ts +++ b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.spec.ts @@ -1,4 +1,3 @@ -import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { YahooFinanceDataEnhancerService } from './yahoo-finance.service'; @@ -26,16 +25,13 @@ jest.mock( ); describe('YahooFinanceDataEnhancerService', () => { - let configurationService: ConfigurationService; let cryptocurrencyService: CryptocurrencyService; let yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService; beforeAll(async () => { - configurationService = new ConfigurationService(); cryptocurrencyService = new CryptocurrencyService(); yahooFinanceDataEnhancerService = new YahooFinanceDataEnhancerService( - configurationService, cryptocurrencyService ); }); diff --git a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts index 1b1335b7e..6090b4f98 100644 --- a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts @@ -1,4 +1,3 @@ -import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { @@ -24,7 +23,6 @@ import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-ifa @Injectable() export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { public constructor( - private readonly configurationService: ConfigurationService, private readonly cryptocurrencyService: CryptocurrencyService ) {} 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 3a840340e..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 @@ -163,10 +163,10 @@ export class EodHistoricalDataService implements DataProviderInterface { ).json(); return response.reduce( - (result, { close, date }) => { - if (isNumber(close)) { + (result, { adjusted_close, date }) => { + if (isNumber(adjusted_close)) { result[this.convertFromEodSymbol(symbol)][date] = { - marketPrice: close + marketPrice: adjusted_close }; } else { Logger.error( @@ -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/src/services/data-provider/google-sheets/google-sheets.service.ts b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts index 966069f22..9f2344233 100644 --- a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts +++ b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts @@ -76,7 +76,7 @@ export class GoogleSheetsService implements DataProviderInterface { } = {}; rows - .filter((row, index) => { + .filter((_row, index) => { return index >= 1; }) .forEach((row) => { diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index d07937787..8d6dd34de 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -30,6 +30,10 @@ export interface Environment extends CleanedEnvAccessors { MAX_ACTIVITIES_TO_IMPORT: number; MAX_CHART_ITEMS: number; PORT: number; + PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY: number; + PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY: number; + PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY: number; + PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT: number; REDIS_DB: number; REDIS_HOST: string; REDIS_PASSWORD: string; diff --git a/apps/api/src/services/market-data/market-data.service.ts b/apps/api/src/services/market-data/market-data.service.ts index 6b77e373c..0d215bb86 100644 --- a/apps/api/src/services/market-data/market-data.service.ts +++ b/apps/api/src/services/market-data/market-data.service.ts @@ -149,21 +149,21 @@ export class MarketDataService { async ({ dataSource, date, marketPrice, symbol, state }) => { return this.prismaService.marketData.upsert({ create: { - dataSource: dataSource, - date: date, - marketPrice: marketPrice, - state: state, - symbol: symbol + dataSource: dataSource as DataSource, + date: date as Date, + marketPrice: marketPrice as number, + state: state as MarketDataState, + symbol: symbol as string }, update: { - marketPrice: marketPrice, - state: state + marketPrice: marketPrice as number, + state: state as MarketDataState }, where: { dataSource_date_symbol: { - dataSource: dataSource, - date: date, - symbol: symbol + dataSource: dataSource as DataSource, + date: date as Date, + symbol: symbol as string } } }); diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts b/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts index d469671ea..bd6b110b0 100644 --- a/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts +++ b/apps/api/src/services/queues/data-gathering/data-gathering.processor.ts @@ -6,8 +6,8 @@ import { import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { DATA_GATHERING_QUEUE, - DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE, - DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA, + DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY, + DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY, GATHER_ASSET_PROFILE_PROCESS, GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME, GATHER_MISSING_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME @@ -45,8 +45,8 @@ export class DataGatheringProcessor { @Process({ concurrency: parseInt( - process.env.PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE ?? - DEFAULT_PROCESSOR_CONCURRENCY_GATHER_ASSET_PROFILE.toString(), + process.env.PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY ?? + DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY.toString(), 10 ), name: GATHER_ASSET_PROFILE_PROCESS @@ -76,8 +76,8 @@ export class DataGatheringProcessor { @Process({ concurrency: parseInt( - process.env.PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA ?? - DEFAULT_PROCESSOR_CONCURRENCY_GATHER_HISTORICAL_MARKET_DATA.toString(), + process.env.PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY ?? + DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY.toString(), 10 ), name: GATHER_HISTORICAL_MARKET_DATA_PROCESS_JOB_NAME @@ -85,7 +85,7 @@ export class DataGatheringProcessor { public async gatherHistoricalMarketData(job: Job) { try { const { dataSource, date, symbol } = job.data; - let currentDate = parseISO((date)); + let currentDate = parseISO(date as unknown as string); Logger.log( `Historical market data gathering has been started for ${symbol} (${dataSource}) at ${format( diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts index 058d971d8..958636334 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts @@ -10,7 +10,7 @@ import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-da import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT, - PORTFOLIO_SNAPSHOT_QUEUE + PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE } from '@ghostfolio/common/config'; import { BullModule } from '@nestjs/bull'; @@ -23,7 +23,7 @@ import { PortfolioSnapshotProcessor } from './portfolio-snapshot.processor'; imports: [ AccountBalanceModule, BullModule.registerQueue({ - name: PORTFOLIO_SNAPSHOT_QUEUE, + name: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE, settings: { lockDuration: parseInt( process.env.PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT ?? diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts index 7c89e9c23..a5a9a37e0 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts @@ -9,9 +9,9 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { CACHE_TTL_INFINITE, - DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT, + DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY, PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME, - PORTFOLIO_SNAPSHOT_QUEUE + PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE } from '@ghostfolio/common/config'; import { Process, Processor } from '@nestjs/bull'; @@ -22,7 +22,7 @@ import { addMilliseconds } from 'date-fns'; import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface'; @Injectable() -@Processor(PORTFOLIO_SNAPSHOT_QUEUE) +@Processor(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE) export class PortfolioSnapshotProcessor { public constructor( private readonly accountBalanceService: AccountBalanceService, @@ -34,8 +34,8 @@ export class PortfolioSnapshotProcessor { @Process({ concurrency: parseInt( - process.env.PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT ?? - DEFAULT_PROCESSOR_CONCURRENCY_PORTFOLIO_SNAPSHOT.toString(), + process.env.PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY ?? + DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY.toString(), 10 ), name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME @@ -94,10 +94,10 @@ export class PortfolioSnapshotProcessor { filters: job.data.filters, userId: job.data.userId }), - JSON.stringify(({ + JSON.stringify({ expiration: expiration.getTime(), portfolioSnapshot: snapshot - })), + } as unknown as PortfolioSnapshotValue), CACHE_TTL_INFINITE ); diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts index 27ebdee53..9dba9275e 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.ts @@ -1,4 +1,4 @@ -import { PORTFOLIO_SNAPSHOT_QUEUE } from '@ghostfolio/common/config'; +import { PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE } from '@ghostfolio/common/config'; import { InjectQueue } from '@nestjs/bull'; import { Injectable } from '@nestjs/common'; @@ -9,7 +9,7 @@ import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queu @Injectable() export class PortfolioSnapshotService { public constructor( - @InjectQueue(PORTFOLIO_SNAPSHOT_QUEUE) + @InjectQueue(PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE) private readonly portfolioSnapshotQueue: Queue ) {} diff --git a/apps/api/src/services/symbol-profile/symbol-profile.service.ts b/apps/api/src/services/symbol-profile/symbol-profile.service.ts index 650414721..0ea5dcff5 100644 --- a/apps/api/src/services/symbol-profile/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile/symbol-profile.service.ts @@ -188,7 +188,7 @@ export class SymbolProfileService { countries: this.getCountries( symbolProfile?.countries as unknown as Prisma.JsonArray ), - dateOfFirstActivity: undefined, + dateOfFirstActivity: undefined as Date, holdings: this.getHoldings(symbolProfile), scraperConfiguration: this.getScraperConfiguration(symbolProfile), sectors: this.getSectors(symbolProfile), 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) { -