Browse Source

Merge branch 'ghostfolio:main' into Overview_Graph

pull/5570/head
Batwam 2 months ago
committed by GitHub
parent
commit
811e2daa4e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      .vscode/extensions.json
  2. 2
      .vscode/settings.json
  3. 113
      CHANGELOG.md
  4. 12
      README.md
  5. 6
      apps/api/src/app/account/account.controller.ts
  6. 6
      apps/api/src/app/account/account.service.ts
  7. 6
      apps/api/src/app/admin/admin.service.ts
  8. 7
      apps/api/src/app/endpoints/market-data/market-data.controller.ts
  9. 6
      apps/api/src/app/endpoints/market-data/market-data.module.ts
  10. 4
      apps/api/src/app/endpoints/platforms/platforms.controller.ts
  11. 6
      apps/api/src/app/import/import.service.ts
  12. 7
      apps/api/src/app/info/info.service.ts
  13. 42
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  14. 15
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts
  15. 16
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
  16. 15
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts
  17. 16
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts
  18. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts
  19. 15
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  20. 18
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  21. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts
  22. 15
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  23. 6
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts
  24. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
  25. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts
  26. 190
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts
  27. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts
  28. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts
  29. 8
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
  30. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts
  31. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  32. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
  33. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts
  34. 33
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  35. 11
      apps/api/src/app/portfolio/current-rate.service.mock.ts
  36. 1
      apps/api/src/app/portfolio/interfaces/portfolio-order-item.interface.ts
  37. 1
      apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts
  38. 5
      apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts
  39. 4
      apps/api/src/app/portfolio/portfolio.controller.ts
  40. 61
      apps/api/src/app/portfolio/portfolio.service.ts
  41. 3
      apps/api/src/app/subscription/subscription.service.ts
  42. 23
      apps/api/src/app/user/user.controller.ts
  43. 4
      apps/api/src/app/user/user.module.ts
  44. 51
      apps/api/src/app/user/user.service.ts
  45. 349
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  46. 139
      apps/api/src/helper/object.helper.spec.ts
  47. 67
      apps/api/src/helper/object.helper.ts
  48. 42
      apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts
  49. 19
      apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
  50. 33
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  51. 8
      apps/api/src/services/tag/tag.service.ts
  52. 6
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  53. 4
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  54. 39
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  55. 8
      apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts
  56. 6
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  57. 6
      apps/client/src/app/components/admin-overview/admin-overview.html
  58. 38
      apps/client/src/app/components/admin-users/admin-users.component.ts
  59. 20
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
  60. 14
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  61. 7
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  62. 2
      apps/client/src/app/components/home-holdings/home-holdings.html
  63. 50
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  64. 1
      apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts
  65. 25
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
  66. 40
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html
  67. 18
      apps/client/src/app/pages/about/overview/about-overview-page.html
  68. 10
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  69. 3
      apps/client/src/app/pages/accounts/accounts-page.html
  70. 11
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  71. 66
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  72. 1
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  73. 9
      apps/client/src/app/pages/pricing/pricing-page.html
  74. 1
      apps/client/src/app/pages/public/public-page.html
  75. BIN
      apps/client/src/assets/images/sponsors/logo-lambdatest.png
  76. 33
      apps/client/src/assets/images/sponsors/logo-testmu-dark.svg
  77. 33
      apps/client/src/assets/images/sponsors/logo-testmu-light.svg
  78. 3
      apps/client/src/environments/environment.prod.ts
  79. 3
      apps/client/src/environments/environment.ts
  80. 406
      apps/client/src/locales/messages.ca.xlf
  81. 406
      apps/client/src/locales/messages.de.xlf
  82. 578
      apps/client/src/locales/messages.es.xlf
  83. 406
      apps/client/src/locales/messages.fr.xlf
  84. 406
      apps/client/src/locales/messages.it.xlf
  85. 548
      apps/client/src/locales/messages.ko.xlf
  86. 406
      apps/client/src/locales/messages.nl.xlf
  87. 406
      apps/client/src/locales/messages.pl.xlf
  88. 406
      apps/client/src/locales/messages.pt.xlf
  89. 406
      apps/client/src/locales/messages.tr.xlf
  90. 406
      apps/client/src/locales/messages.uk.xlf
  91. 400
      apps/client/src/locales/messages.xlf
  92. 432
      apps/client/src/locales/messages.zh.xlf
  93. 4
      libs/common/eslint.config.cjs
  94. 4
      libs/common/src/lib/calculation-helper.ts
  95. 57
      libs/common/src/lib/chart-helper.ts
  96. 2
      libs/common/src/lib/class-transformer.ts
  97. 52
      libs/common/src/lib/config.ts
  98. 22
      libs/common/src/lib/helper.ts
  99. 2
      libs/common/src/lib/interfaces/activities.interface.ts
  100. 2
      libs/common/src/lib/interfaces/admin-data.interface.ts

4
.vscode/extensions.json

@ -1,8 +1,8 @@
{ {
"recommendations": [ "recommendations": [
"angular.ng-template", "angular.ng-template",
"esbenp.prettier-vscode",
"firsttris.vscode-jest-runner", "firsttris.vscode-jest-runner",
"nrwl.angular-console", "nrwl.angular-console"
"prettier.prettier-vscode"
] ]
} }

2
.vscode/settings.json

@ -1,4 +1,4 @@
{ {
"editor.defaultFormatter": "prettier.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true "editor.formatOnSave": true
} }

113
CHANGELOG.md

@ -5,12 +5,123 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## 2.238.0 - 2026-02-12
### Changed
- Upgraded `ngx-skeleton-loader` from version `11.3.0` to `12.0.0`
- Upgraded `twitter-api-v2` from version `1.27.0` to `1.29.0`
### Fixed
- Fixed a performance calculation issue by resetting tracking variables when a holding is fully closed
- Fixed an issue in the annualized performance calculation
- Fixed an issue with the exchange rate calculation by expanding the date range to cover the full day (start to end of day)
## 2.237.0 - 2026-02-08
### Changed
- Removed the deprecated `transactionCount` in the portfolio calculator and service
- Refreshed the cryptocurrencies list
- Upgraded `Nx` from version `22.4.1` to `22.4.5`
### Fixed
- Fixed the accounts of the assistant for the impersonation mode
- Fixed the tags of the assistant for the impersonation mode
## 2.236.0 - 2026-02-05
### Changed
- Removed the deprecated `transactionCount` in the endpoint `GET api/v1/admin`
- Upgraded `stripe` from version `20.1.0` to `20.3.0`
### Fixed
- Fixed an exception when fetching the top holdings for ETF and mutual fund assets from _Yahoo Finance_
## 2.235.0 - 2026-02-03
### Added
- Added the ability to fetch top holdings for ETF and mutual fund assets from _Yahoo Finance_
- Added support for the impersonation mode in the endpoint `GET api/v1/account/:id/balances`
- Added an action menu to the user detail dialog in the users section of the admin control panel
### Changed
- Optimized the value redaction interceptor for the impersonation mode by introducing `fast-redact`
- Refactored `showTransactions` in favor of `showActivitiesCount` in the accounts table component
- Refactored `transactionCount` in favor of `activitiesCount` in the accounts table component
- Deprecated `transactionCount` in favor of `activitiesCount` in the endpoint `GET api/v1/admin`
- Removed the deprecated `firstBuyDate` in the portfolio calculator
- Upgraded `yahoo-finance2` from version `3.11.2` to `3.13.0`
## 2.234.0 - 2026-01-30
### Changed
- Improved the usability of the create asset profile dialog in the market data section of the admin control panel
- Improved the language localization for Chinese (`zh`)
- Improved the language localization for German (`de`)
- Improved the language localization for Spanish (`es`)
- Upgraded `angular` from version `21.0.6` to `21.1.1`
- Upgraded `lodash` from version `4.17.21` to `4.17.23`
- Upgraded `Nx` from version `22.3.3` to `22.4.1`
- Upgraded `prettier` from version `3.8.0` to `3.8.1`
## 2.233.0 - 2026-01-23
### Changed
- Deprecated `firstBuyDate` in favor of `dateOfFirstActivity` in the portfolio calculator
- Deprecated `transactionCount` in favor of `activitiesCount` in the portfolio calculator and service
- Removed the deprecated `firstBuyDate` from the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol`
- Refreshed the cryptocurrencies list
- Upgraded `prettier` from version `3.7.4` to `3.8.0`
## 2.232.0 - 2026-01-19
### Added
- Extended the analysis page to include the total amount, change and performance with currency effects (experimental)
### Changed
- Deprecated `firstBuyDate` in favor of `dateOfFirstActivity` in the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol`
- Improved the language localization for German (`de`)
- Upgraded `countries-list` from version `3.2.0` to `3.2.2`
## 2.231.0 - 2026-01-17
### Changed
- Removed the deprecated platforms from the info service
- Removed the deprecated activities from the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol`
### Fixed
- Fixed a numeric parsing error related to cash positions on the _X-ray_ page
- Fixed the total fee calculation in the holding detail dialog related to activities in a custom currency
- Fixed the total fee calculation in the summary related to activities in a custom currency
## 2.230.0 - 2026-01-14
### Added ### Added
- Set up the language localization for Korean (`ko`) - Set up the language localization for Korean (`ko`)
### Changed
- Restored the support for specific calendar year date ranges (`2024`, `2023`, `2022`, etc.) in the holdings table (experimental)
### Fixed
- Fixed the total fee calculation in the holding detail dialog related to activities in a custom currency
- Fixed the total fee calculation in the summary related to activities in a custom currency
## 2.229.0 - 2026-01-11 ## 2.229.0 - 2026-01-11
### Changed ### Changed

12
README.md

@ -86,11 +86,12 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
### Supported Environment Variables ### Supported Environment Variables
| Name | Type | Default Value | Description | | Name | Type | Default Value | Description |
| ------------------------ | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | --------------------------- | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens | | `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens |
| `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key | | `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key |
| `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key | | `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key |
| `DATABASE_URL` | `string` | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` | | `DATABASE_URL` | `string` | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` |
| `ENABLE_FEATURE_AUTH_TOKEN` | `boolean` (optional) | `true` | Enables authentication via security token |
| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on | | `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on |
| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) | | `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) |
| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` | | `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` |
@ -109,7 +110,7 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
| Name | Type | Default Value | Description | | Name | Type | Default Value | Description |
| -------------------------- | --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------- | | -------------------------- | --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `ENABLE_FEATURE_AUTH_OIDC` | `boolean` (optional) | `false` | Enables _OpenID Connect_ authentication | | `ENABLE_FEATURE_AUTH_OIDC` | `boolean` (optional) | `false` | Enables authentication via _OpenID Connect_ |
| `OIDC_AUTHORIZATION_URL` | `string` (optional) | | Manual override for the OIDC authorization endpoint (falls back to the discovery from the issuer) | | `OIDC_AUTHORIZATION_URL` | `string` (optional) | | Manual override for the OIDC authorization endpoint (falls back to the discovery from the issuer) |
| `OIDC_CALLBACK_URL` | `string` (optional) | `${ROOT_URL}/api/auth/oidc/callback` | The OIDC callback URL | | `OIDC_CALLBACK_URL` | `string` (optional) | `${ROOT_URL}/api/auth/oidc/callback` | The OIDC callback URL |
| `OIDC_CLIENT_ID` | `string` | | The OIDC client ID | | `OIDC_CLIENT_ID` | `string` | | The OIDC client ID |
@ -317,12 +318,9 @@ If you like to support this project, become a [**Sponsor**](https://github.com/s
## Sponsors ## Sponsors
<div align="center"> <div align="center">
<p> <a href="https://www.testmuai.com?utm_medium=sponsor&utm_source=ghostfolio" target="_blank" title="TestMu AI - AI Powered Testing Tool">
Browser testing via<br /> <img alt="TestMu AI Logo" height="45" src="https://assets.testmuai.com/resources/images/logos/logo.svg" />
<a href="https://www.lambdatest.com?utm_medium=sponsor&utm_source=ghostfolio" target="_blank" title="LambdaTest - AI Powered Testing Tool">
<img alt="LambdaTest Logo" height="45" width="250" src="https://www.lambdatest.com/blue-logo.png" />
</a> </a>
</p>
</div> </div>
## Analytics ## Analytics

6
apps/api/src/app/account/account.controller.ts

@ -132,12 +132,16 @@ export class AccountController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
public async getAccountBalancesById( public async getAccountBalancesById(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('id') id: string @Param('id') id: string
): Promise<AccountBalancesResponse> { ): Promise<AccountBalancesResponse> {
const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId);
return this.accountBalanceService.getAccountBalances({ return this.accountBalanceService.getAccountBalances({
filters: [{ id, type: 'ACCOUNT' }], filters: [{ id, type: 'ACCOUNT' }],
userCurrency: this.request.user.settings.settings.baseCurrency, userCurrency: this.request.user.settings.settings.baseCurrency,
userId: this.request.user.id userId: impersonationUserId || this.request.user.id
}); });
} }

6
apps/api/src/app/account/account.service.ts

@ -150,15 +150,15 @@ export class AccountService {
}); });
return accounts.map((account) => { return accounts.map((account) => {
let transactionCount = 0; let activitiesCount = 0;
for (const { isDraft } of account.activities) { for (const { isDraft } of account.activities) {
if (!isDraft) { if (!isDraft) {
transactionCount += 1; activitiesCount += 1;
} }
} }
const result = { ...account, transactionCount }; const result = { ...account, activitiesCount };
delete result.activities; delete result.activities;

6
apps/api/src/app/admin/admin.service.ts

@ -138,11 +138,11 @@ export class AdminService {
public async get(): Promise<AdminData> { public async get(): Promise<AdminData> {
const dataSources = Object.values(DataSource); const dataSources = Object.values(DataSource);
const [enabledDataSources, settings, transactionCount, userCount] = const [activitiesCount, enabledDataSources, settings, userCount] =
await Promise.all([ await Promise.all([
this.prismaService.order.count(),
this.dataProviderService.getDataSources(), this.dataProviderService.getDataSources(),
this.propertyService.get(), this.propertyService.get(),
this.prismaService.order.count(),
this.countUsersWithAnalytics() this.countUsersWithAnalytics()
]); ]);
@ -182,9 +182,9 @@ export class AdminService {
).filter(Boolean); ).filter(Boolean);
return { return {
activitiesCount,
dataProviders, dataProviders,
settings, settings,
transactionCount,
userCount, userCount,
version: environment.version version: environment.version
}; };

7
apps/api/src/app/endpoints/market-data/market-data.controller.ts

@ -2,6 +2,8 @@ import { AdminService } from '@ghostfolio/api/app/admin/admin.service';
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
@ -28,7 +30,8 @@ import {
Param, Param,
Post, Post,
Query, Query,
UseGuards UseGuards,
UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
@ -86,6 +89,8 @@ export class MarketDataController {
@Get(':dataSource/:symbol') @Get(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getMarketDataBySymbol( public async getMarketDataBySymbol(
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string

6
apps/api/src/app/endpoints/market-data/market-data.module.ts

@ -1,5 +1,7 @@
import { AdminModule } from '@ghostfolio/api/app/admin/admin.module'; import { AdminModule } from '@ghostfolio/api/app/admin/admin.module';
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -13,7 +15,9 @@ import { MarketDataController } from './market-data.controller';
AdminModule, AdminModule,
MarketDataServiceModule, MarketDataServiceModule,
SymbolModule, SymbolModule,
SymbolProfileModule SymbolProfileModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
] ]
}) })
export class MarketDataModule {} export class MarketDataModule {}

4
apps/api/src/app/endpoints/platforms/platforms.controller.ts

@ -15,7 +15,9 @@ export class PlatformsController {
@HasPermission(permissions.readPlatforms) @HasPermission(permissions.readPlatforms)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getPlatforms(): Promise<PlatformsResponse> { public async getPlatforms(): Promise<PlatformsResponse> {
const platforms = await this.platformService.getPlatforms(); const platforms = await this.platformService.getPlatforms({
orderBy: { name: 'asc' }
});
return { platforms }; return { platforms };
} }

6
apps/api/src/app/import/import.service.ts

@ -82,7 +82,7 @@ export class ImportService {
filterBySymbol: symbol filterBySymbol: symbol
}); });
const { firstBuyDate, historicalData } = holding; const { dateOfFirstActivity, historicalData } = holding;
const [{ accounts }, { activities }, [assetProfile], dividends] = const [{ accounts }, { activities }, [assetProfile], dividends] =
await Promise.all([ await Promise.all([
@ -95,7 +95,7 @@ export class ImportService {
filters, filters,
userCurrency, userCurrency,
userId, userId,
startDate: parseDate(firstBuyDate) startDate: parseDate(dateOfFirstActivity)
}), }),
this.symbolProfileService.getSymbolProfiles([ this.symbolProfileService.getSymbolProfiles([
{ {
@ -106,7 +106,7 @@ export class ImportService {
await this.dataProviderService.getDividends({ await this.dataProviderService.getDividends({
dataSource, dataSource,
symbol, symbol,
from: parseDate(firstBuyDate), from: parseDate(dateOfFirstActivity),
granularity: 'day', granularity: 'day',
to: new Date() to: new Date()
}) })

7
apps/api/src/app/info/info.service.ts

@ -1,4 +1,3 @@
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
@ -38,7 +37,6 @@ export class InfoService {
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly platformService: PlatformService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
private readonly redisCacheService: RedisCacheService, private readonly redisCacheService: RedisCacheService,
private readonly subscriptionService: SubscriptionService, private readonly subscriptionService: SubscriptionService,
@ -103,16 +101,12 @@ export class InfoService {
benchmarks, benchmarks,
demoAuthToken, demoAuthToken,
isUserSignupEnabled, isUserSignupEnabled,
platforms,
statistics, statistics,
subscriptionOffer subscriptionOffer
] = await Promise.all([ ] = await Promise.all([
this.benchmarkService.getBenchmarkAssetProfiles(), this.benchmarkService.getBenchmarkAssetProfiles(),
this.getDemoAuthToken(), this.getDemoAuthToken(),
this.propertyService.isUserSignupEnabled(), this.propertyService.isUserSignupEnabled(),
this.platformService.getPlatforms({
orderBy: { name: 'asc' }
}),
this.getStatistics(), this.getStatistics(),
this.subscriptionService.getSubscriptionOffer({ key: 'default' }) this.subscriptionService.getSubscriptionOffer({ key: 'default' })
]); ]);
@ -127,7 +121,6 @@ export class InfoService {
demoAuthToken, demoAuthToken,
globalPermissions, globalPermissions,
isReadOnlyMode, isReadOnlyMode,
platforms,
statistics, statistics,
subscriptionOffer, subscriptionOffer,
baseCurrency: DEFAULT_CURRENCY, baseCurrency: DEFAULT_CURRENCY,

42
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -53,6 +53,7 @@ import {
isBefore, isBefore,
isWithinInterval, isWithinInterval,
min, min,
startOfDay,
startOfYear, startOfYear,
subDays subDays
} from 'date-fns'; } from 'date-fns';
@ -120,6 +121,7 @@ export abstract class PortfolioCalculator {
({ ({
date, date,
feeInAssetProfileCurrency, feeInAssetProfileCurrency,
feeInBaseCurrency,
quantity, quantity,
SymbolProfile, SymbolProfile,
tags = [], tags = [],
@ -142,6 +144,7 @@ export abstract class PortfolioCalculator {
type, type,
date: format(date, DATE_FORMAT), date: format(date, DATE_FORMAT),
fee: new Big(feeInAssetProfileCurrency), fee: new Big(feeInAssetProfileCurrency),
feeInBaseCurrency: new Big(feeInBaseCurrency),
quantity: new Big(quantity), quantity: new Big(quantity),
unitPrice: new Big(unitPriceInAssetProfileCurrency) unitPrice: new Big(unitPriceInAssetProfileCurrency)
}; };
@ -160,8 +163,8 @@ export abstract class PortfolioCalculator {
subDays(dateOfFirstActivity, 1) subDays(dateOfFirstActivity, 1)
); );
this.endDate = endDate; this.endDate = endOfDay(endDate);
this.startDate = startDate; this.startDate = startOfDay(startDate);
this.computeTransactionPoints(); this.computeTransactionPoints();
@ -234,7 +237,7 @@ export abstract class PortfolioCalculator {
const exchangeRatesByCurrency = const exchangeRatesByCurrency =
await this.exchangeRateDataService.getExchangeRatesByCurrency({ await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: Array.from(new Set(Object.values(currencies))), currencies: Array.from(new Set(Object.values(currencies))),
endDate: endOfDay(this.endDate), endDate: this.endDate,
startDate: this.startDate, startDate: this.startDate,
targetCurrency: this.currency targetCurrency: this.currency
}); });
@ -336,12 +339,6 @@ export abstract class PortfolioCalculator {
} = {}; } = {};
for (const item of lastTransactionPoint.items) { for (const item of lastTransactionPoint.items) {
const feeInBaseCurrency = item.fee.mul(
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
lastTransactionPoint.date
] ?? 1
);
const marketPriceInBaseCurrency = ( const marketPriceInBaseCurrency = (
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice
).mul( ).mul(
@ -408,17 +405,18 @@ export abstract class PortfolioCalculator {
} }
positions.push({ positions.push({
feeInBaseCurrency,
includeInTotalAssetValue, includeInTotalAssetValue,
timeWeightedInvestment, timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect, timeWeightedInvestmentWithCurrencyEffect,
activitiesCount: item.activitiesCount,
averagePrice: item.averagePrice, averagePrice: item.averagePrice,
currency: item.currency, currency: item.currency,
dataSource: item.dataSource, dataSource: item.dataSource,
dateOfFirstActivity: item.dateOfFirstActivity,
dividend: totalDividend, dividend: totalDividend,
dividendInBaseCurrency: totalDividendInBaseCurrency, dividendInBaseCurrency: totalDividendInBaseCurrency,
fee: item.fee, fee: item.fee,
firstBuyDate: item.firstBuyDate, feeInBaseCurrency: item.feeInBaseCurrency,
grossPerformance: !hasErrors ? (grossPerformance ?? null) : null, grossPerformance: !hasErrors ? (grossPerformance ?? null) : null,
grossPerformancePercentage: !hasErrors grossPerformancePercentage: !hasErrors
? (grossPerformancePercentage ?? null) ? (grossPerformancePercentage ?? null)
@ -433,9 +431,8 @@ export abstract class PortfolioCalculator {
investment: totalInvestment, investment: totalInvestment,
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
marketPrice: marketPrice:
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null, marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? 1,
marketPriceInBaseCurrency: marketPriceInBaseCurrency: marketPriceInBaseCurrency?.toNumber() ?? 1,
marketPriceInBaseCurrency?.toNumber() ?? null,
netPerformance: !hasErrors ? (netPerformance ?? null) : null, netPerformance: !hasErrors ? (netPerformance ?? null) : null,
netPerformancePercentage: !hasErrors netPerformancePercentage: !hasErrors
? (netPerformancePercentage ?? null) ? (netPerformancePercentage ?? null)
@ -449,7 +446,6 @@ export abstract class PortfolioCalculator {
quantity: item.quantity, quantity: item.quantity,
symbol: item.symbol, symbol: item.symbol,
tags: item.tags, tags: item.tags,
transactionCount: item.transactionCount,
valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul( valueInBaseCurrency: new Big(marketPriceInBaseCurrency).mul(
item.quantity item.quantity
) )
@ -938,6 +934,7 @@ export abstract class PortfolioCalculator {
for (const { for (const {
date, date,
fee, fee,
feeInBaseCurrency,
quantity, quantity,
SymbolProfile, SymbolProfile,
tags, tags,
@ -997,16 +994,18 @@ export abstract class PortfolioCalculator {
investment, investment,
skipErrors, skipErrors,
symbol, symbol,
activitiesCount: oldAccumulatedSymbol.activitiesCount + 1,
averagePrice: newQuantity.eq(0) averagePrice: newQuantity.eq(0)
? new Big(0) ? new Big(0)
: investment.div(newQuantity).abs(), : investment.div(newQuantity).abs(),
dateOfFirstActivity: oldAccumulatedSymbol.dateOfFirstActivity,
dividend: new Big(0), dividend: new Big(0),
fee: oldAccumulatedSymbol.fee.plus(fee), fee: oldAccumulatedSymbol.fee.plus(fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate, feeInBaseCurrency:
oldAccumulatedSymbol.feeInBaseCurrency.plus(feeInBaseCurrency),
includeInHoldings: oldAccumulatedSymbol.includeInHoldings, includeInHoldings: oldAccumulatedSymbol.includeInHoldings,
quantity: newQuantity, quantity: newQuantity,
tags: oldAccumulatedSymbol.tags.concat(tags), tags: oldAccumulatedSymbol.tags.concat(tags)
transactionCount: oldAccumulatedSymbol.transactionCount + 1
}; };
} else { } else {
currentTransactionPointItem = { currentTransactionPointItem = {
@ -1014,16 +1013,17 @@ export abstract class PortfolioCalculator {
currency, currency,
dataSource, dataSource,
fee, fee,
feeInBaseCurrency,
skipErrors, skipErrors,
symbol, symbol,
tags, tags,
activitiesCount: 1,
averagePrice: unitPrice, averagePrice: unitPrice,
dateOfFirstActivity: date,
dividend: new Big(0), dividend: new Big(0),
firstBuyDate: date,
includeInHoldings: INVESTMENT_ACTIVITY_TYPES.includes(type), includeInHoldings: INVESTMENT_ACTIVITY_TYPES.includes(type),
investment: unitPrice.mul(quantity).mul(factor), investment: unitPrice.mul(quantity).mul(factor),
quantity: quantity.mul(factor), quantity: quantity.mul(factor)
transactionCount: 1
}; };
} }

15
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-22'), date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55, feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -102,6 +103,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65, feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -131,20 +133,26 @@ describe('PortfolioCalculator', () => {
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('595.6'), currentValueInBaseCurrency: new Big('595.6'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 2,
averagePrice: new Big('139.75'), averagePrice: new Big('139.75'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2021-11-22',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'), fee: new Big('3.2'),
feeInBaseCurrency: new Big('3.2'), feeInBaseCurrency: new Big('3.2'),
firstBuyDate: '2021-11-22',
grossPerformance: new Big('36.6'), grossPerformance: new Big('36.6'),
grossPerformancePercentage: new Big('0.07706261539956593567'), grossPerformancePercentage: new Big('0.07706261539956593567'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
@ -170,7 +178,6 @@ describe('PortfolioCalculator', () => {
timeWeightedInvestmentWithCurrencyEffect: new Big( timeWeightedInvestmentWithCurrencyEffect: new Big(
'474.93846153846153846154' '474.93846153846153846154'
), ),
transactionCount: 2,
valueInBaseCurrency: new Big('595.6') valueInBaseCurrency: new Big('595.6')
} }
], ],
@ -200,6 +207,10 @@ describe('PortfolioCalculator', () => {
{ date: '2021-11-01', investment: 559 }, { date: '2021-11-01', investment: 559 },
{ date: '2021-12-01', investment: 0 } { date: '2021-12-01', investment: 0 }
]); ]);
expect(investmentsByYear).toEqual([
{ date: '2021-01-01', investment: 559 }
]);
}); });
}); });
}); });

16
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-22'), date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55, feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -102,6 +103,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65, feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -117,6 +119,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -146,20 +149,26 @@ describe('PortfolioCalculator', () => {
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'), currentValueInBaseCurrency: new Big('0'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 3,
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2021-11-22',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'), fee: new Big('3.2'),
feeInBaseCurrency: new Big('3.2'), feeInBaseCurrency: new Big('3.2'),
firstBuyDate: '2021-11-22',
grossPerformance: new Big('-12.6'), grossPerformance: new Big('-12.6'),
grossPerformancePercentage: new Big('-0.04408677396780965649'), grossPerformancePercentage: new Big('-0.04408677396780965649'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
@ -183,7 +192,6 @@ describe('PortfolioCalculator', () => {
timeWeightedInvestmentWithCurrencyEffect: new Big( timeWeightedInvestmentWithCurrencyEffect: new Big(
'285.80000000000000396627' '285.80000000000000396627'
), ),
transactionCount: 3,
valueInBaseCurrency: new Big('0') valueInBaseCurrency: new Big('0')
} }
], ],
@ -213,6 +221,10 @@ describe('PortfolioCalculator', () => {
{ date: '2021-11-01', investment: 0 }, { date: '2021-11-01', investment: 0 },
{ date: '2021-12-01', investment: 0 } { date: '2021-12-01', investment: 0 }
]); ]);
expect(investmentsByYear).toEqual([
{ date: '2021-01-01', investment: 0 }
]);
}); });
}); });
}); });

15
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-22'), date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55, feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -102,6 +103,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65, feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -131,20 +133,26 @@ describe('PortfolioCalculator', () => {
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'), currentValueInBaseCurrency: new Big('0'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 2,
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2021-11-22',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'), fee: new Big('3.2'),
feeInBaseCurrency: new Big('3.2'), feeInBaseCurrency: new Big('3.2'),
firstBuyDate: '2021-11-22',
grossPerformance: new Big('-12.6'), grossPerformance: new Big('-12.6'),
grossPerformancePercentage: new Big('-0.0440867739678096571'), grossPerformancePercentage: new Big('-0.0440867739678096571'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
@ -168,7 +176,6 @@ describe('PortfolioCalculator', () => {
tags: [], tags: [],
timeWeightedInvestment: new Big('285.8'), timeWeightedInvestment: new Big('285.8'),
timeWeightedInvestmentWithCurrencyEffect: new Big('285.8'), timeWeightedInvestmentWithCurrencyEffect: new Big('285.8'),
transactionCount: 2,
valueInBaseCurrency: new Big('0') valueInBaseCurrency: new Big('0')
} }
], ],
@ -198,6 +205,10 @@ describe('PortfolioCalculator', () => {
{ date: '2021-11-01', investment: 0 }, { date: '2021-11-01', investment: 0 },
{ date: '2021-12-01', investment: 0 } { date: '2021-12-01', investment: 0 }
]); ]);
expect(investmentsByYear).toEqual([
{ date: '2021-01-01', investment: 0 }
]);
}); });
}); });
}); });

16
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55, feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -122,20 +123,26 @@ describe('PortfolioCalculator', () => {
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('297.8'), currentValueInBaseCurrency: new Big('297.8'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 1,
averagePrice: new Big('136.6'), averagePrice: new Big('136.6'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2021-11-30',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('1.55'), fee: new Big('1.55'),
feeInBaseCurrency: new Big('1.55'), feeInBaseCurrency: new Big('1.55'),
firstBuyDate: '2021-11-30',
grossPerformance: new Big('24.6'), grossPerformance: new Big('24.6'),
grossPerformancePercentage: new Big('0.09004392386530014641'), grossPerformancePercentage: new Big('0.09004392386530014641'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
@ -165,7 +172,6 @@ describe('PortfolioCalculator', () => {
tags: [], tags: [],
timeWeightedInvestment: new Big('273.2'), timeWeightedInvestment: new Big('273.2'),
timeWeightedInvestmentWithCurrencyEffect: new Big('273.2'), timeWeightedInvestmentWithCurrencyEffect: new Big('273.2'),
transactionCount: 1,
valueInBaseCurrency: new Big('297.8') valueInBaseCurrency: new Big('297.8')
} }
], ],
@ -198,6 +204,10 @@ describe('PortfolioCalculator', () => {
{ date: '2021-11-01', investment: 273.2 }, { date: '2021-11-01', investment: 273.2 },
{ date: '2021-12-01', investment: 0 } { date: '2021-12-01', investment: 0 }
]); ]);
expect(investmentsByYear).toEqual([
{ date: '2021-01-01', investment: 273.2 }
]);
}); });
it.only('with BALN.SW buy (with unit price lower than closing price)', async () => { it.only('with BALN.SW buy (with unit price lower than closing price)', async () => {
@ -208,6 +218,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55, feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -247,6 +258,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55, feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,

1
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts

@ -110,6 +110,7 @@ describe('PortfolioCalculator', () => {
...activity, ...activity,
date: parseDate(activity.date), date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46, feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 3.94,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
currency: 'USD', currency: 'USD',

15
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts

@ -98,6 +98,7 @@ describe('PortfolioCalculator', () => {
...activity, ...activity,
date: parseDate(activity.date), date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46, feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 4.46,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
@ -131,6 +132,11 @@ describe('PortfolioCalculator', () => {
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot.historicalData[0]).toEqual({ expect(portfolioSnapshot.historicalData[0]).toEqual({
date: '2021-12-11', date: '2021-12-11',
investmentValueWithCurrencyEffect: 0, investmentValueWithCurrencyEffect: 0,
@ -189,14 +195,15 @@ describe('PortfolioCalculator', () => {
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 1,
averagePrice: new Big('44558.42'), averagePrice: new Big('44558.42'),
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2021-12-12',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('4.46'), fee: new Big('4.46'),
feeInBaseCurrency: new Big('4.46'), feeInBaseCurrency: new Big('4.46'),
firstBuyDate: '2021-12-12',
grossPerformance: new Big('-1458.72'), grossPerformance: new Big('-1458.72'),
grossPerformancePercentage: new Big('-0.03273724696701543726'), grossPerformancePercentage: new Big('-0.03273724696701543726'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
@ -220,7 +227,6 @@ describe('PortfolioCalculator', () => {
tags: [], tags: [],
timeWeightedInvestment: new Big('44558.42'), timeWeightedInvestment: new Big('44558.42'),
timeWeightedInvestmentWithCurrencyEffect: new Big('44558.42'), timeWeightedInvestmentWithCurrencyEffect: new Big('44558.42'),
transactionCount: 1,
valueInBaseCurrency: new Big('43099.7') valueInBaseCurrency: new Big('43099.7')
} }
], ],
@ -244,6 +250,11 @@ describe('PortfolioCalculator', () => {
{ date: '2021-12-01', investment: 44558.42 }, { date: '2021-12-01', investment: 44558.42 },
{ date: '2022-01-01', investment: 0 } { date: '2022-01-01', investment: 0 }
]); ]);
expect(investmentsByYear).toEqual([
{ date: '2021-01-01', investment: 44558.42 },
{ date: '2022-01-01', investment: 0 }
]);
}); });
}); });
}); });

18
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts

@ -100,6 +100,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2015-01-01'), date: new Date('2015-01-01'),
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -115,6 +116,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2017-12-31'), date: new Date('2017-12-31'),
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -144,6 +146,11 @@ describe('PortfolioCalculator', () => {
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('13298.425356'), currentValueInBaseCurrency: new Big('13298.425356'),
errors: [], errors: [],
@ -151,14 +158,15 @@ describe('PortfolioCalculator', () => {
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 2,
averagePrice: new Big('320.43'), averagePrice: new Big('320.43'),
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2015-01-01',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('0'), fee: new Big('0'),
feeInBaseCurrency: new Big('0'), feeInBaseCurrency: new Big('0'),
firstBuyDate: '2015-01-01',
grossPerformance: new Big('27172.74').mul(0.97373), grossPerformance: new Big('27172.74').mul(0.97373),
grossPerformancePercentage: new Big('0.4241983590271396608571'), grossPerformancePercentage: new Big('0.4241983590271396608571'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
@ -186,7 +194,6 @@ describe('PortfolioCalculator', () => {
timeWeightedInvestmentWithCurrencyEffect: new Big( timeWeightedInvestmentWithCurrencyEffect: new Big(
'636.79389574611155533947' '636.79389574611155533947'
), ),
transactionCount: 2,
valueInBaseCurrency: new Big('13298.425356') valueInBaseCurrency: new Big('13298.425356')
} }
], ],
@ -251,6 +258,13 @@ describe('PortfolioCalculator', () => {
{ date: '2017-12-01', investment: -318.54266729999995 }, { date: '2017-12-01', investment: -318.54266729999995 },
{ date: '2018-01-01', investment: 0 } { date: '2018-01-01', investment: 0 }
]); ]);
expect(investmentsByYear).toEqual([
{ date: '2015-01-01', investment: 637.0853345999999 },
{ date: '2016-01-01', investment: 0 },
{ date: '2017-01-01', investment: -318.54266729999995 },
{ date: '2018-01-01', investment: 0 }
]);
}); });
}); });
}); });

1
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts

@ -98,6 +98,7 @@ describe('PortfolioCalculator', () => {
...activity, ...activity,
date: parseDate(activity.date), date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee, feeInAssetProfileCurrency: activity.fee,
feeInBaseCurrency: activity.fee,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
currency: 'USD', currency: 'USD',

15
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts

@ -98,6 +98,7 @@ describe('PortfolioCalculator', () => {
...activity, ...activity,
date: parseDate(activity.date), date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46, feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 4.46,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
@ -131,6 +132,11 @@ describe('PortfolioCalculator', () => {
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot.historicalData[0]).toEqual({ expect(portfolioSnapshot.historicalData[0]).toEqual({
date: '2021-12-11', date: '2021-12-11',
investmentValueWithCurrencyEffect: 0, investmentValueWithCurrencyEffect: 0,
@ -189,14 +195,15 @@ describe('PortfolioCalculator', () => {
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 1,
averagePrice: new Big('44558.42'), averagePrice: new Big('44558.42'),
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2021-12-12',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('4.46'), fee: new Big('4.46'),
feeInBaseCurrency: new Big('4.46'), feeInBaseCurrency: new Big('4.46'),
firstBuyDate: '2021-12-12',
grossPerformance: new Big('-1458.72'), grossPerformance: new Big('-1458.72'),
grossPerformancePercentage: new Big('-0.03273724696701543726'), grossPerformancePercentage: new Big('-0.03273724696701543726'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
@ -220,7 +227,6 @@ describe('PortfolioCalculator', () => {
tags: [], tags: [],
timeWeightedInvestment: new Big('44558.42'), timeWeightedInvestment: new Big('44558.42'),
timeWeightedInvestmentWithCurrencyEffect: new Big('44558.42'), timeWeightedInvestmentWithCurrencyEffect: new Big('44558.42'),
transactionCount: 1,
valueInBaseCurrency: new Big('43099.7') valueInBaseCurrency: new Big('43099.7')
} }
], ],
@ -244,6 +250,11 @@ describe('PortfolioCalculator', () => {
{ date: '2021-12-01', investment: 44558.42 }, { date: '2021-12-01', investment: 44558.42 },
{ date: '2022-01-01', investment: 0 } { date: '2022-01-01', investment: 0 }
]); ]);
expect(investmentsByYear).toEqual([
{ date: '2021-01-01', investment: 44558.42 },
{ date: '2022-01-01', investment: 0 }
]);
}); });
}); });
}); });

6
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts

@ -230,14 +230,15 @@ describe('PortfolioCalculator', () => {
* Value in base currency: 2000 USD * 0.91 = 1820 CHF * Value in base currency: 2000 USD * 0.91 = 1820 CHF
*/ */
expect(position).toMatchObject<TimelinePosition>({ expect(position).toMatchObject<TimelinePosition>({
activitiesCount: 2,
averagePrice: new Big(1), averagePrice: new Big(1),
currency: 'USD', currency: 'USD',
dataSource: DataSource.YAHOO, dataSource: DataSource.YAHOO,
dateOfFirstActivity: '2023-12-31',
dividend: new Big(0), dividend: new Big(0),
dividendInBaseCurrency: new Big(0), dividendInBaseCurrency: new Big(0),
fee: new Big(0), fee: new Big(0),
feeInBaseCurrency: new Big(0), feeInBaseCurrency: new Big(0),
firstBuyDate: '2023-12-31',
grossPerformance: new Big(0), grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0), grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
@ -247,7 +248,7 @@ describe('PortfolioCalculator', () => {
includeInTotalAssetValue: false, includeInTotalAssetValue: false,
investment: new Big(1820), investment: new Big(1820),
investmentWithCurrencyEffect: new Big(1750), investmentWithCurrencyEffect: new Big(1750),
marketPrice: null, marketPrice: 1,
marketPriceInBaseCurrency: 0.91, marketPriceInBaseCurrency: 0.91,
netPerformance: new Big(0), netPerformance: new Big(0),
netPerformancePercentage: new Big(0), netPerformancePercentage: new Big(0),
@ -275,7 +276,6 @@ describe('PortfolioCalculator', () => {
timeWeightedInvestmentWithCurrencyEffect: new Big( timeWeightedInvestmentWithCurrencyEffect: new Big(
'852.45231607629427792916' '852.45231607629427792916'
), ),
transactionCount: 2,
valueInBaseCurrency: new Big(1820) valueInBaseCurrency: new Big(1820)
}); });

1
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-09-01'), date: new Date('2021-09-01'),
feeInAssetProfileCurrency: 49, feeInAssetProfileCurrency: 49,
feeInBaseCurrency: 49,
quantity: 0, quantity: 0,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,

14
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts

@ -99,6 +99,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2023-01-03'), date: new Date('2023-01-03'),
feeInAssetProfileCurrency: 1, feeInAssetProfileCurrency: 1,
feeInBaseCurrency: 0.9238,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -128,20 +129,26 @@ describe('PortfolioCalculator', () => {
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('103.10483'), currentValueInBaseCurrency: new Big('103.10483'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 1,
averagePrice: new Big('89.12'), averagePrice: new Big('89.12'),
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2023-01-03',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('1'), fee: new Big('1'),
feeInBaseCurrency: new Big('0.9238'), feeInBaseCurrency: new Big('0.9238'),
firstBuyDate: '2023-01-03',
grossPerformance: new Big('27.33').mul(0.8854), grossPerformance: new Big('27.33').mul(0.8854),
grossPerformancePercentage: new Big('0.3066651705565529623'), grossPerformancePercentage: new Big('0.3066651705565529623'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
@ -165,7 +172,6 @@ describe('PortfolioCalculator', () => {
tags: [], tags: [],
timeWeightedInvestment: new Big('89.12').mul(0.8854), timeWeightedInvestment: new Big('89.12').mul(0.8854),
timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'), timeWeightedInvestmentWithCurrencyEffect: new Big('82.329056'),
transactionCount: 1,
valueInBaseCurrency: new Big('103.10483') valueInBaseCurrency: new Big('103.10483')
} }
], ],
@ -217,6 +223,10 @@ describe('PortfolioCalculator', () => {
investment: 0 investment: 0
} }
]); ]);
expect(investmentsByYear).toEqual([
{ date: '2023-01-01', investment: 82.329056 }
]);
}); });
}); });
}); });

190
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts

@ -0,0 +1,190 @@
import {
activityDummyData,
loadExportFile,
symbolProfileDummyData,
userDummyData
} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper';
import { Activity, ExportResponse } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Big } from 'big.js';
import { join } from 'node:path';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
jest.mock(
'@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service',
() => {
return {
PortfolioSnapshotService: jest.fn().mockImplementation(() => {
return PortfolioSnapshotServiceMock;
})
};
}
);
jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => {
return {
RedisCacheService: jest.fn().mockImplementation(() => {
return RedisCacheServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let exportResponse: ExportResponse;
let configurationService: ConfigurationService;
let currentRateService: CurrentRateService;
let exchangeRateDataService: ExchangeRateDataService;
let portfolioCalculatorFactory: PortfolioCalculatorFactory;
let portfolioSnapshotService: PortfolioSnapshotService;
let redisCacheService: RedisCacheService;
beforeAll(() => {
exportResponse = loadExportFile(
join(
__dirname,
'../../../../../../../test/import/ok/jnug-buy-and-sell-and-buy-and-sell.json'
)
);
});
beforeEach(() => {
configurationService = new ConfigurationService();
currentRateService = new CurrentRateService(null, null, null, null);
exchangeRateDataService = new ExchangeRateDataService(
null,
null,
null,
null
);
portfolioSnapshotService = new PortfolioSnapshotService(null);
redisCacheService = new RedisCacheService(null, null);
portfolioCalculatorFactory = new PortfolioCalculatorFactory(
configurationService,
currentRateService,
exchangeRateDataService,
portfolioSnapshotService,
redisCacheService
);
});
describe('get current positions', () => {
it.only('with JNUG buy and sell', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2025-12-28').getTime());
const activities: Activity[] = exportResponse.activities.map(
(activity) => ({
...activityDummyData,
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
feeInBaseCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: activity.currency,
dataSource: activity.dataSource,
name: 'Direxion Daily Junior Gold Miners Index Bull 2X Shares',
symbol: activity.symbol
},
unitPriceInAssetProfileCurrency: activity.unitPrice
})
);
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: exportResponse.user.settings.currency,
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'month'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'),
errors: [],
hasErrors: false,
positions: [
{
activitiesCount: 4,
averagePrice: new Big('0'),
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: '2025-12-11',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('4'),
feeInBaseCurrency: new Big('4'),
grossPerformance: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10)
grossPerformanceWithCurrencyEffect: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10)
investment: new Big('0'),
investmentWithCurrencyEffect: new Big('0'),
netPerformance: new Big('39.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4
netPerformanceWithCurrencyEffectMap: {
max: new Big('39.95') // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4
},
marketPrice: 237.8000030517578,
marketPriceInBaseCurrency: 237.8000030517578,
quantity: new Big('0'),
symbol: 'JNUG',
tags: [],
valueInBaseCurrency: new Big('0')
}
],
totalFeesWithCurrencyEffect: new Big('4'),
totalInterestWithCurrencyEffect: new Big('0'),
totalInvestment: new Big('0'),
totalInvestmentWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0')
});
expect(investments).toEqual([
{ date: '2025-12-11', investment: new Big('1885.05') },
{ date: '2025-12-18', investment: new Big('2041.1') },
{ date: '2025-12-28', investment: new Big('0') }
]);
expect(investmentsByMonth).toEqual([
{ date: '2025-12-01', investment: 0 }
]);
expect(investmentsByYear).toEqual([
{ date: '2025-01-01', investment: 0 }
]);
});
});
});

1
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2023-01-01'), // Date in future date: new Date('2023-01-01'), // Date in future
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,

7
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts

@ -80,6 +80,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2024-03-08'), date: new Date('2024-03-08'),
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 0.3333333333333333, quantity: 0.3333333333333333,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -94,8 +95,9 @@ describe('PortfolioCalculator', () => {
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2024-03-13'), date: new Date('2024-03-13'),
quantity: 0.6666666666666666,
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 0.6666666666666666,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
currency: 'USD', currency: 'USD',
@ -109,8 +111,9 @@ describe('PortfolioCalculator', () => {
{ {
...activityDummyData, ...activityDummyData,
date: new Date('2024-03-14'), date: new Date('2024-03-14'),
quantity: 1,
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
currency: 'USD', currency: 'USD',

8
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-09-16'), date: new Date('2021-09-16'),
feeInAssetProfileCurrency: 19, feeInAssetProfileCurrency: 19,
feeInBaseCurrency: 19,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -102,6 +103,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-16'), date: new Date('2021-11-16'),
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -129,13 +131,14 @@ describe('PortfolioCalculator', () => {
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 2,
averagePrice: new Big('298.58'), averagePrice: new Big('298.58'),
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2021-09-16',
dividend: new Big('0.62'), dividend: new Big('0.62'),
dividendInBaseCurrency: new Big('0.62'), dividendInBaseCurrency: new Big('0.62'),
fee: new Big('19'), fee: new Big('19'),
firstBuyDate: '2021-09-16',
grossPerformance: new Big('33.25'), grossPerformance: new Big('33.25'),
grossPerformancePercentage: new Big('0.11136043941322258691'), grossPerformancePercentage: new Big('0.11136043941322258691'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
@ -159,8 +162,7 @@ describe('PortfolioCalculator', () => {
}, },
quantity: new Big('1'), quantity: new Big('1'),
symbol: 'MSFT', symbol: 'MSFT',
tags: [], tags: []
transactionCount: 2
} }
], ],
totalFeesWithCurrencyEffect: new Big('19'), totalFeesWithCurrencyEffect: new Big('19'),

7
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts

@ -93,6 +93,11 @@ describe('PortfolioCalculator', () => {
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big(0), currentValueInBaseCurrency: new Big(0),
hasErrors: false, hasErrors: false,
@ -108,6 +113,8 @@ describe('PortfolioCalculator', () => {
expect(investments).toEqual([]); expect(investments).toEqual([]);
expect(investmentsByMonth).toEqual([]); expect(investmentsByMonth).toEqual([]);
expect(investmentsByYear).toEqual([]);
}); });
}); });
}); });

14
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts

@ -101,6 +101,7 @@ describe('PortfolioCalculator', () => {
...activity, ...activity,
date: parseDate(activity.date), date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee, feeInAssetProfileCurrency: activity.fee,
feeInBaseCurrency: activity.fee,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
currency: activity.currency, currency: activity.currency,
@ -128,20 +129,26 @@ describe('PortfolioCalculator', () => {
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('87.8'), currentValueInBaseCurrency: new Big('87.8'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 2,
averagePrice: new Big('75.80'), averagePrice: new Big('75.80'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2022-03-07',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('4.25'), fee: new Big('4.25'),
feeInBaseCurrency: new Big('4.25'), feeInBaseCurrency: new Big('4.25'),
firstBuyDate: '2022-03-07',
grossPerformance: new Big('21.93'), grossPerformance: new Big('21.93'),
grossPerformancePercentage: new Big('0.15113417083448194384'), grossPerformancePercentage: new Big('0.15113417083448194384'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
@ -167,7 +174,6 @@ describe('PortfolioCalculator', () => {
timeWeightedInvestmentWithCurrencyEffect: new Big( timeWeightedInvestmentWithCurrencyEffect: new Big(
'145.10285714285714285714' '145.10285714285714285714'
), ),
transactionCount: 2,
valueInBaseCurrency: new Big('87.8') valueInBaseCurrency: new Big('87.8')
} }
], ],
@ -197,6 +203,10 @@ describe('PortfolioCalculator', () => {
{ date: '2022-03-01', investment: 151.6 }, { date: '2022-03-01', investment: 151.6 },
{ date: '2022-04-01', investment: -75.8 } { date: '2022-04-01', investment: -75.8 }
]); ]);
expect(investmentsByYear).toEqual([
{ date: '2022-01-01', investment: 75.8 }
]);
}); });
}); });
}); });

14
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts

@ -101,6 +101,7 @@ describe('PortfolioCalculator', () => {
...activity, ...activity,
date: parseDate(activity.date), date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee, feeInAssetProfileCurrency: activity.fee,
feeInBaseCurrency: activity.fee,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
currency: activity.currency, currency: activity.currency,
@ -128,6 +129,11 @@ describe('PortfolioCalculator', () => {
groupBy: 'month' groupBy: 'month'
}); });
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot.historicalData[0]).toEqual({ expect(portfolioSnapshot.historicalData[0]).toEqual({
date: '2022-03-06', date: '2022-03-06',
investmentValueWithCurrencyEffect: 0, investmentValueWithCurrencyEffect: 0,
@ -187,14 +193,15 @@ describe('PortfolioCalculator', () => {
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 2,
averagePrice: new Big('0'), averagePrice: new Big('0'),
currency: 'CHF', currency: 'CHF',
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2022-03-07',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('0'), fee: new Big('0'),
feeInBaseCurrency: new Big('0'), feeInBaseCurrency: new Big('0'),
firstBuyDate: '2022-03-07',
grossPerformance: new Big('19.86'), grossPerformance: new Big('19.86'),
grossPerformancePercentage: new Big('0.13100263852242744063'), grossPerformancePercentage: new Big('0.13100263852242744063'),
grossPerformancePercentageWithCurrencyEffect: new Big( grossPerformancePercentageWithCurrencyEffect: new Big(
@ -218,7 +225,6 @@ describe('PortfolioCalculator', () => {
tags: [], tags: [],
timeWeightedInvestment: new Big('151.6'), timeWeightedInvestment: new Big('151.6'),
timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'),
transactionCount: 2,
valueInBaseCurrency: new Big('0') valueInBaseCurrency: new Big('0')
} }
], ],
@ -248,6 +254,10 @@ describe('PortfolioCalculator', () => {
{ date: '2022-03-01', investment: 151.6 }, { date: '2022-03-01', investment: 151.6 },
{ date: '2022-04-01', investment: -151.6 } { date: '2022-04-01', investment: -151.6 }
]); ]);
expect(investmentsByYear).toEqual([
{ date: '2022-01-01', investment: 0 }
]);
}); });
}); });
}); });

7
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2022-01-01'), date: new Date('2022-01-01'),
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -115,21 +116,22 @@ describe('PortfolioCalculator', () => {
hasErrors: false, hasErrors: false,
positions: [ positions: [
{ {
activitiesCount: 1,
averagePrice: new Big('500000'), averagePrice: new Big('500000'),
currency: 'USD', currency: 'USD',
dataSource: 'MANUAL', dataSource: 'MANUAL',
dateOfFirstActivity: '2022-01-01',
dividend: new Big('0'), dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'), dividendInBaseCurrency: new Big('0'),
fee: new Big('0'), fee: new Big('0'),
feeInBaseCurrency: new Big('0'), feeInBaseCurrency: new Big('0'),
firstBuyDate: '2022-01-01',
grossPerformance: new Big('0'), grossPerformance: new Big('0'),
grossPerformancePercentage: new Big('0'), grossPerformancePercentage: new Big('0'),
grossPerformancePercentageWithCurrencyEffect: new Big('0'), grossPerformancePercentageWithCurrencyEffect: new Big('0'),
grossPerformanceWithCurrencyEffect: new Big('0'), grossPerformanceWithCurrencyEffect: new Big('0'),
investment: new Big('500000'), investment: new Big('500000'),
investmentWithCurrencyEffect: new Big('500000'), investmentWithCurrencyEffect: new Big('500000'),
marketPrice: null, marketPrice: 1,
marketPriceInBaseCurrency: 500000, marketPriceInBaseCurrency: 500000,
netPerformance: new Big('0'), netPerformance: new Big('0'),
netPerformancePercentage: new Big('0'), netPerformancePercentage: new Big('0'),
@ -144,7 +146,6 @@ describe('PortfolioCalculator', () => {
tags: [], tags: [],
timeWeightedInvestment: new Big('500000'), timeWeightedInvestment: new Big('500000'),
timeWeightedInvestmentWithCurrencyEffect: new Big('500000'), timeWeightedInvestmentWithCurrencyEffect: new Big('500000'),
transactionCount: 1,
valueInBaseCurrency: new Big('500000') valueInBaseCurrency: new Big('500000')
} }
], ],

33
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

@ -13,7 +13,14 @@ import { PerformanceCalculationType } from '@ghostfolio/common/types/performance
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { addMilliseconds, differenceInDays, format, isBefore } from 'date-fns'; import {
addMilliseconds,
differenceInDays,
eachYearOfInterval,
format,
isBefore,
isThisYear
} from 'date-fns';
import { cloneDeep, sortBy } from 'lodash'; import { cloneDeep, sortBy } from 'lodash';
export class RoaiPortfolioCalculator extends PortfolioCalculator { export class RoaiPortfolioCalculator extends PortfolioCalculator {
@ -619,6 +626,13 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
totalQuantityFromBuyTransactions totalQuantityFromBuyTransactions
); );
if (totalUnits.eq(0)) {
// Reset tracking variables when position is fully closed
totalInvestmentFromBuyTransactions = new Big(0);
totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
totalQuantityFromBuyTransactions = new Big(0);
}
if (PortfolioCalculator.ENABLE_LOGGING) { if (PortfolioCalculator.ENABLE_LOGGING) {
console.log( console.log(
'grossPerformanceFromSells', 'grossPerformanceFromSells',
@ -837,15 +851,14 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
'max', 'max',
'mtd', 'mtd',
'wtd', 'wtd',
'ytd' 'ytd',
// TODO: ...eachYearOfInterval({ end, start })
// ...eachYearOfInterval({ end, start }) .filter((date) => {
// .filter((date) => { return !isThisYear(date);
// return !isThisYear(date); })
// }) .map((date) => {
// .map((date) => { return format(date, 'yyyy');
// return format(date, 'yyyy'); })
// })
] as DateRange[]) { ] as DateRange[]) {
const dateInterval = getIntervalFromDateRange(dateRange); const dateInterval = getIntervalFromDateRange(dateRange);
const endDate = dateInterval.endDate; const endDate = dateInterval.endDate;

11
apps/api/src/app/portfolio/current-rate.service.mock.ts

@ -64,6 +64,17 @@ function mockGetValue(symbol: string, date: Date) {
return { marketPrice: 0 }; return { marketPrice: 0 };
case 'JNUG':
if (isSameDay(parseDate('2025-12-10'), date)) {
return { marketPrice: 204.5599975585938 };
} else if (isSameDay(parseDate('2025-12-17'), date)) {
return { marketPrice: 203.9700012207031 };
} else if (isSameDay(parseDate('2025-12-28'), date)) {
return { marketPrice: 237.8000030517578 };
}
return { marketPrice: 0 };
case 'MSFT': case 'MSFT':
if (isSameDay(parseDate('2021-09-16'), date)) { if (isSameDay(parseDate('2021-09-16'), date)) {
return { marketPrice: 89.12 }; return { marketPrice: 89.12 };

1
apps/api/src/app/portfolio/interfaces/portfolio-order-item.interface.ts

@ -3,7 +3,6 @@ import { Big } from 'big.js';
import { PortfolioOrder } from './portfolio-order.interface'; import { PortfolioOrder } from './portfolio-order.interface';
export interface PortfolioOrderItem extends PortfolioOrder { export interface PortfolioOrderItem extends PortfolioOrder {
feeInBaseCurrency?: Big;
feeInBaseCurrencyWithCurrencyEffect?: Big; feeInBaseCurrencyWithCurrencyEffect?: Big;
itemType?: 'end' | 'start'; itemType?: 'end' | 'start';
unitPriceFromMarketData?: Big; unitPriceFromMarketData?: Big;

1
apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts

@ -3,6 +3,7 @@ import { Activity } from '@ghostfolio/common/interfaces';
export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> { export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> {
date: string; date: string;
fee: Big; fee: Big;
feeInBaseCurrency: Big;
quantity: Big; quantity: Big;
SymbolProfile: Pick< SymbolProfile: Pick<
Activity['SymbolProfile'], Activity['SymbolProfile'],

5
apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts

@ -2,18 +2,19 @@ import { AssetSubClass, DataSource, Tag } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
export interface TransactionPointSymbol { export interface TransactionPointSymbol {
activitiesCount: number;
assetSubClass: AssetSubClass; assetSubClass: AssetSubClass;
averagePrice: Big; averagePrice: Big;
currency: string; currency: string;
dataSource: DataSource; dataSource: DataSource;
dateOfFirstActivity: string;
dividend: Big; dividend: Big;
fee: Big; fee: Big;
firstBuyDate: string; feeInBaseCurrency: Big;
includeInHoldings: boolean; includeInHoldings: boolean;
investment: Big; investment: Big;
quantity: Big; quantity: Big;
skipErrors: boolean; skipErrors: boolean;
symbol: string; symbol: string;
tags?: Tag[]; tags?: Tag[];
transactionCount: number;
} }

4
apps/api/src/app/portfolio/portfolio.controller.ts

@ -195,11 +195,13 @@ export class PortfolioController {
'excludedAccountsAndActivities', 'excludedAccountsAndActivities',
'fees', 'fees',
'filteredValueInBaseCurrency', 'filteredValueInBaseCurrency',
'fireWealth',
'grossPerformance', 'grossPerformance',
'grossPerformanceWithCurrencyEffect', 'grossPerformanceWithCurrencyEffect',
'interestInBaseCurrency', 'interestInBaseCurrency',
'items', 'items',
'liabilities', 'liabilities',
'liabilitiesInBaseCurrency',
'netPerformance', 'netPerformance',
'netPerformanceWithCurrencyEffect', 'netPerformanceWithCurrencyEffect',
'totalBuy', 'totalBuy',
@ -329,7 +331,7 @@ export class PortfolioController {
types: ['DIVIDEND'] types: ['DIVIDEND']
}); });
let dividends = await this.portfolioService.getDividends({ let dividends = this.portfolioService.getDividends({
activities, activities,
groupBy groupBy
}); });

61
apps/api/src/app/portfolio/portfolio.service.ts

@ -179,9 +179,9 @@ export class PortfolioService {
return Promise.all( return Promise.all(
accounts.map(async (account) => { accounts.map(async (account) => {
let activitiesCount = 0;
let dividendInBaseCurrency = 0; let dividendInBaseCurrency = 0;
let interestInBaseCurrency = 0; let interestInBaseCurrency = 0;
let transactionCount = 0;
for (const { for (const {
currency, currency,
@ -214,7 +214,7 @@ export class PortfolioService {
} }
if (!isDraft) { if (!isDraft) {
transactionCount += 1; activitiesCount += 1;
} }
} }
@ -223,9 +223,9 @@ export class PortfolioService {
const result = { const result = {
...account, ...account,
activitiesCount,
dividendInBaseCurrency, dividendInBaseCurrency,
interestInBaseCurrency, interestInBaseCurrency,
transactionCount,
valueInBaseCurrency, valueInBaseCurrency,
allocationInPercentage: 0, allocationInPercentage: 0,
balanceInBaseCurrency: this.exchangeRateDataService.toCurrency( balanceInBaseCurrency: this.exchangeRateDataService.toCurrency(
@ -262,6 +262,8 @@ export class PortfolioService {
withExcludedAccounts withExcludedAccounts
}); });
let activitiesCount = 0;
const searchQuery = filters.find(({ type }) => { const searchQuery = filters.find(({ type }) => {
return type === 'SEARCH_QUERY'; return type === 'SEARCH_QUERY';
})?.id; })?.id;
@ -281,9 +283,10 @@ export class PortfolioService {
let totalDividendInBaseCurrency = new Big(0); let totalDividendInBaseCurrency = new Big(0);
let totalInterestInBaseCurrency = new Big(0); let totalInterestInBaseCurrency = new Big(0);
let totalValueInBaseCurrency = new Big(0); let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0;
for (const account of accounts) { for (const account of accounts) {
activitiesCount += account.activitiesCount;
totalBalanceInBaseCurrency = totalBalanceInBaseCurrency.plus( totalBalanceInBaseCurrency = totalBalanceInBaseCurrency.plus(
account.balanceInBaseCurrency account.balanceInBaseCurrency
); );
@ -296,7 +299,6 @@ export class PortfolioService {
totalValueInBaseCurrency = totalValueInBaseCurrency.plus( totalValueInBaseCurrency = totalValueInBaseCurrency.plus(
account.valueInBaseCurrency account.valueInBaseCurrency
); );
transactionCount += account.transactionCount;
} }
for (const account of accounts) { for (const account of accounts) {
@ -310,7 +312,7 @@ export class PortfolioService {
return { return {
accounts, accounts,
transactionCount, activitiesCount,
totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(), totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(),
totalDividendInBaseCurrency: totalDividendInBaseCurrency.toNumber(), totalDividendInBaseCurrency: totalDividendInBaseCurrency.toNumber(),
totalInterestInBaseCurrency: totalInterestInBaseCurrency.toNumber(), totalInterestInBaseCurrency: totalInterestInBaseCurrency.toNumber(),
@ -318,13 +320,13 @@ export class PortfolioService {
}; };
} }
public async getDividends({ public getDividends({
activities, activities,
groupBy groupBy
}: { }: {
activities: Activity[]; activities: Activity[];
groupBy?: GroupBy; groupBy?: GroupBy;
}): Promise<InvestmentItem[]> { }): InvestmentItem[] {
let dividends = activities.map(({ currency, date, value }) => { let dividends = activities.map(({ currency, date, value }) => {
return { return {
date: format(date, DATE_FORMAT), date: format(date, DATE_FORMAT),
@ -567,9 +569,10 @@ export class PortfolioService {
} }
for (const { for (const {
activitiesCount,
currency, currency,
dateOfFirstActivity,
dividend, dividend,
firstBuyDate,
grossPerformance, grossPerformance,
grossPerformanceWithCurrencyEffect, grossPerformanceWithCurrencyEffect,
grossPerformancePercentage, grossPerformancePercentage,
@ -583,7 +586,6 @@ export class PortfolioService {
quantity, quantity,
symbol, symbol,
tags, tags,
transactionCount,
valueInBaseCurrency valueInBaseCurrency
} of positions) { } of positions) {
if (isFilteredByClosedHoldings === true) { if (isFilteredByClosedHoldings === true) {
@ -610,13 +612,13 @@ export class PortfolioService {
} }
holdings[symbol] = { holdings[symbol] = {
activitiesCount,
currency, currency,
markets, markets,
marketsAdvanced, marketsAdvanced,
marketPrice, marketPrice,
symbol, symbol,
tags, tags,
transactionCount,
allocationInPercentage: filteredValueInBaseCurrency.eq(0) allocationInPercentage: filteredValueInBaseCurrency.eq(0)
? 0 ? 0
: valueInBaseCurrency.div(filteredValueInBaseCurrency).toNumber(), : valueInBaseCurrency.div(filteredValueInBaseCurrency).toNumber(),
@ -624,7 +626,7 @@ export class PortfolioService {
assetSubClass: assetProfile.assetSubClass, assetSubClass: assetProfile.assetSubClass,
countries: assetProfile.countries, countries: assetProfile.countries,
dataSource: assetProfile.dataSource, dataSource: assetProfile.dataSource,
dateOfFirstActivity: parseDate(firstBuyDate), dateOfFirstActivity: parseDate(dateOfFirstActivity),
dividend: dividend?.toNumber() ?? 0, dividend: dividend?.toNumber() ?? 0,
grossPerformance: grossPerformance?.toNumber() ?? 0, grossPerformance: grossPerformance?.toNumber() ?? 0,
grossPerformancePercent: grossPerformancePercentage?.toNumber() ?? 0, grossPerformancePercent: grossPerformancePercentage?.toNumber() ?? 0,
@ -789,11 +791,12 @@ export class PortfolioService {
} }
const { const {
activitiesCount,
averagePrice, averagePrice,
currency, currency,
dateOfFirstActivity,
dividendInBaseCurrency, dividendInBaseCurrency,
fee, feeInBaseCurrency,
firstBuyDate,
grossPerformance, grossPerformance,
grossPerformancePercentage, grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect, grossPerformancePercentageWithCurrencyEffect,
@ -807,8 +810,7 @@ export class PortfolioService {
quantity, quantity,
tags, tags,
timeWeightedInvestment, timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect, timeWeightedInvestmentWithCurrencyEffect
transactionCount
} = holding; } = holding;
const activitiesOfHolding = activities.filter(({ SymbolProfile }) => { const activitiesOfHolding = activities.filter(({ SymbolProfile }) => {
@ -819,7 +821,10 @@ export class PortfolioService {
}); });
const dividendYieldPercent = getAnnualizedPerformancePercent({ const dividendYieldPercent = getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), daysInMarket: differenceInDays(
new Date(),
parseDate(dateOfFirstActivity)
),
netPerformancePercentage: timeWeightedInvestment.eq(0) netPerformancePercentage: timeWeightedInvestment.eq(0)
? new Big(0) ? new Big(0)
: dividendInBaseCurrency.div(timeWeightedInvestment) : dividendInBaseCurrency.div(timeWeightedInvestment)
@ -827,7 +832,10 @@ export class PortfolioService {
const dividendYieldPercentWithCurrencyEffect = const dividendYieldPercentWithCurrencyEffect =
getAnnualizedPerformancePercent({ getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), daysInMarket: differenceInDays(
new Date(),
parseDate(dateOfFirstActivity)
),
netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(0) netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(0)
? new Big(0) ? new Big(0)
: dividendInBaseCurrency.div(timeWeightedInvestmentWithCurrencyEffect) : dividendInBaseCurrency.div(timeWeightedInvestmentWithCurrencyEffect)
@ -836,7 +844,7 @@ export class PortfolioService {
const historicalData = await this.dataProviderService.getHistorical( const historicalData = await this.dataProviderService.getHistorical(
[{ dataSource, symbol }], [{ dataSource, symbol }],
'day', 'day',
parseISO(firstBuyDate), parseISO(dateOfFirstActivity),
new Date() new Date()
); );
@ -901,7 +909,7 @@ export class PortfolioService {
// Add historical entry for buy date, if no historical data available // Add historical entry for buy date, if no historical data available
historicalDataArray.push({ historicalDataArray.push({
averagePrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, averagePrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
date: firstBuyDate, date: dateOfFirstActivity,
marketPrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, marketPrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency,
quantity: activitiesOfHolding[0].quantity quantity: activitiesOfHolding[0].quantity
}); });
@ -914,25 +922,20 @@ export class PortfolioService {
); );
return { return {
firstBuyDate, activitiesCount,
dateOfFirstActivity,
marketPrice, marketPrice,
marketPriceMax, marketPriceMax,
marketPriceMin, marketPriceMin,
SymbolProfile, SymbolProfile,
tags, tags,
activities: activitiesOfHolding,
activitiesCount: transactionCount,
averagePrice: averagePrice.toNumber(), averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
dividendYieldPercent: dividendYieldPercent.toNumber(), dividendYieldPercent: dividendYieldPercent.toNumber(),
dividendYieldPercentWithCurrencyEffect: dividendYieldPercentWithCurrencyEffect:
dividendYieldPercentWithCurrencyEffect.toNumber(), dividendYieldPercentWithCurrencyEffect.toNumber(),
feeInBaseCurrency: this.exchangeRateDataService.toCurrency( feeInBaseCurrency: feeInBaseCurrency.toNumber(),
fee.toNumber(),
SymbolProfile.currency,
userCurrency
),
grossPerformance: grossPerformance?.toNumber(), grossPerformance: grossPerformance?.toNumber(),
grossPerformancePercent: grossPerformancePercentage?.toNumber(), grossPerformancePercent: grossPerformancePercentage?.toNumber(),
grossPerformancePercentWithCurrencyEffect: grossPerformancePercentWithCurrencyEffect:
@ -1662,6 +1665,7 @@ export class PortfolioService {
}): PortfolioPosition { }): PortfolioPosition {
return { return {
currency, currency,
activitiesCount: 0,
allocationInPercentage: 0, allocationInPercentage: 0,
assetClass: AssetClass.LIQUIDITY, assetClass: AssetClass.LIQUIDITY,
assetSubClass: AssetSubClass.CASH, assetSubClass: AssetSubClass.CASH,
@ -1685,7 +1689,6 @@ export class PortfolioService {
sectors: [], sectors: [],
symbol: currency, symbol: currency,
tags: [], tags: [],
transactionCount: 0,
valueInBaseCurrency: balance valueInBaseCurrency: balance
}; };
} }

3
apps/api/src/app/subscription/subscription.service.ts

@ -35,7 +35,7 @@ export class SubscriptionService {
this.stripe = new Stripe( this.stripe = new Stripe(
this.configurationService.get('STRIPE_SECRET_KEY'), this.configurationService.get('STRIPE_SECRET_KEY'),
{ {
apiVersion: '2025-12-15.clover' apiVersion: '2026-01-28.clover'
} }
); );
} }
@ -100,7 +100,6 @@ export class SubscriptionService {
); );
return { return {
sessionId: session.id,
sessionUrl: session.url sessionUrl: session.url
}; };
} }

23
apps/api/src/app/user/user.controller.ts

@ -1,8 +1,11 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import { import {
DeleteOwnUserDto, DeleteOwnUserDto,
UpdateOwnAccessTokenDto, UpdateOwnAccessTokenDto,
@ -28,7 +31,8 @@ import {
Param, Param,
Post, Post,
Put, Put,
UseGuards UseGuards,
UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
@ -43,6 +47,7 @@ import { UserService } from './user.service';
export class UserController { export class UserController {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly impersonationService: ImpersonationService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
@ -107,13 +112,19 @@ export class UserController {
@Get() @Get()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(RedactValuesInResponseInterceptor)
public async getUser( public async getUser(
@Headers('accept-language') acceptLanguage: string @Headers('accept-language') acceptLanguage: string,
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string
): Promise<User> { ): Promise<User> {
return this.userService.getUser( const impersonationUserId =
this.request.user, await this.impersonationService.validateImpersonationId(impersonationId);
acceptLanguage?.split(',')?.[0]
); return this.userService.getUser({
impersonationUserId,
locale: acceptLanguage?.split(',')?.[0],
user: this.request.user
});
} }
@Post() @Post()

4
apps/api/src/app/user/user.module.ts

@ -1,7 +1,9 @@
import { OrderModule } from '@ghostfolio/api/app/order/order.module'; import { OrderModule } from '@ghostfolio/api/app/order/order.module';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { I18nModule } from '@ghostfolio/api/services/i18n/i18n.module'; import { I18nModule } from '@ghostfolio/api/services/i18n/i18n.module';
import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { TagModule } from '@ghostfolio/api/services/tag/tag.module'; import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
@ -18,6 +20,7 @@ import { UserService } from './user.service';
imports: [ imports: [
ConfigurationModule, ConfigurationModule,
I18nModule, I18nModule,
ImpersonationModule,
JwtModule.register({ JwtModule.register({
secret: process.env.JWT_SECRET_KEY, secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '30 days' } signOptions: { expiresIn: '30 days' }
@ -25,6 +28,7 @@ import { UserService } from './user.service';
OrderModule, OrderModule,
PrismaModule, PrismaModule,
PropertyModule, PropertyModule,
RedactValuesInResponseModule,
SubscriptionModule, SubscriptionModule,
TagModule TagModule
], ],

51
apps/api/src/app/user/user.service.ts

@ -30,7 +30,7 @@ import {
PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_SYSTEM_MESSAGE, PROPERTY_SYSTEM_MESSAGE,
TAG_ID_EXCLUDE_FROM_ANALYSIS, TAG_ID_EXCLUDE_FROM_ANALYSIS,
locale locale as defaultLocale
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
User as IUser, User as IUser,
@ -49,7 +49,7 @@ import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { Prisma, Role, User } from '@prisma/client'; import { Prisma, Role, User } from '@prisma/client';
import { differenceInDays, subDays } from 'date-fns'; import { differenceInDays, subDays } from 'date-fns';
import { sortBy, without } from 'lodash'; import { without } from 'lodash';
import { createHmac } from 'node:crypto'; import { createHmac } from 'node:crypto';
@Injectable() @Injectable()
@ -96,10 +96,17 @@ export class UserService {
return { accessToken, hashedAccessToken }; return { accessToken, hashedAccessToken };
} }
public async getUser( public async getUser({
{ accounts, id, permissions, settings, subscription }: UserWithSettings, impersonationUserId,
aLocale = locale locale = defaultLocale,
): Promise<IUser> { user
}: {
impersonationUserId: string;
locale?: string;
user: UserWithSettings;
}): Promise<IUser> {
const { id, permissions, settings, subscription } = user;
const userData = await Promise.all([ const userData = await Promise.all([
this.prismaService.access.findMany({ this.prismaService.access.findMany({
include: { include: {
@ -108,22 +115,31 @@ export class UserService {
orderBy: { alias: 'asc' }, orderBy: { alias: 'asc' },
where: { granteeUserId: id } where: { granteeUserId: id }
}), }),
this.prismaService.account.findMany({
orderBy: {
name: 'asc'
},
where: {
userId: impersonationUserId || user.id
}
}),
this.prismaService.order.count({ this.prismaService.order.count({
where: { userId: id } where: { userId: impersonationUserId || user.id }
}), }),
this.prismaService.order.findFirst({ this.prismaService.order.findFirst({
orderBy: { orderBy: {
date: 'asc' date: 'asc'
}, },
where: { userId: id } where: { userId: impersonationUserId || user.id }
}), }),
this.tagService.getTagsForUser(id) this.tagService.getTagsForUser(impersonationUserId || user.id)
]); ]);
const access = userData[0]; const access = userData[0];
const activitiesCount = userData[1]; const accounts = userData[1];
const firstActivity = userData[2]; const activitiesCount = userData[2];
let tags = userData[3].filter((tag) => { const firstActivity = userData[3];
let tags = userData[4].filter((tag) => {
return tag.id !== TAG_ID_EXCLUDE_FROM_ANALYSIS; return tag.id !== TAG_ID_EXCLUDE_FROM_ANALYSIS;
}); });
@ -146,7 +162,6 @@ export class UserService {
} }
return { return {
accounts,
activitiesCount, activitiesCount,
id, id,
permissions, permissions,
@ -160,10 +175,13 @@ export class UserService {
permissions: accessItem.permissions permissions: accessItem.permissions
}; };
}), }),
accounts: accounts.sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
}),
dateOfFirstActivity: firstActivity?.date ?? new Date(), dateOfFirstActivity: firstActivity?.date ?? new Date(),
settings: { settings: {
...(settings.settings as UserSettings), ...(settings.settings as UserSettings),
locale: (settings.settings as UserSettings)?.locale ?? aLocale locale: (settings.settings as UserSettings)?.locale ?? locale
} }
}; };
} }
@ -516,9 +534,10 @@ export class UserService {
currentPermissions.push(permissions.impersonateAllUsers); currentPermissions.push(permissions.impersonateAllUsers);
} }
user.accounts = sortBy(user.accounts, ({ name }) => { user.accounts = user.accounts.sort((a, b) => {
return name.toLowerCase(); return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
}); });
user.permissions = currentPermissions.sort(); user.permissions = currentPermissions.sort();
return user; return user;

349
apps/api/src/assets/cryptocurrencies/cryptocurrencies.json

File diff suppressed because it is too large

139
apps/api/src/helper/object.helper.spec.ts

@ -1,4 +1,6 @@
import { query, redactAttributes } from './object.helper'; import { DEFAULT_REDACTED_PATHS } from '@ghostfolio/common/config';
import { query, redactPaths } from './object.helper';
describe('query', () => { describe('query', () => {
it('should get market price from stock API response', () => { it('should get market price from stock API response', () => {
@ -22,46 +24,38 @@ describe('query', () => {
describe('redactAttributes', () => { describe('redactAttributes', () => {
it('should redact provided attributes', () => { it('should redact provided attributes', () => {
expect(redactAttributes({ object: {}, options: [] })).toStrictEqual({}); expect(redactPaths({ object: {}, paths: [] })).toStrictEqual({});
expect( expect(redactPaths({ object: { value: 1000 }, paths: [] })).toStrictEqual({
redactAttributes({ object: { value: 1000 }, options: [] }) value: 1000
).toStrictEqual({ value: 1000 }); });
expect( expect(
redactAttributes({ redactPaths({
object: { value: 1000 }, object: { value: 1000 },
options: [{ attribute: 'value', valueMap: { '*': null } }] paths: ['value']
}) })
).toStrictEqual({ value: null }); ).toStrictEqual({ value: null });
expect( expect(
redactAttributes({ redactPaths({
object: { value: 'abc' }, object: { value: 'abc' },
options: [{ attribute: 'value', valueMap: { abc: 'xyz' } }] paths: ['value'],
valueMap: { abc: 'xyz' }
}) })
).toStrictEqual({ value: 'xyz' }); ).toStrictEqual({ value: 'xyz' });
expect( expect(
redactAttributes({ redactPaths({
object: { data: [{ value: 'a' }, { value: 'b' }] }, object: { data: [{ value: 'a' }, { value: 'b' }] },
options: [{ attribute: 'value', valueMap: { a: 1, b: 2 } }] paths: ['data[*].value'],
valueMap: { a: 1, b: 2 }
}) })
).toStrictEqual({ data: [{ value: 1 }, { value: 2 }] }); ).toStrictEqual({ data: [{ value: 1 }, { value: 2 }] });
expect(
redactAttributes({
object: { value1: 'a', value2: 'b' },
options: [
{ attribute: 'value1', valueMap: { a: 'x' } },
{ attribute: 'value2', valueMap: { '*': 'y' } }
]
})
).toStrictEqual({ value1: 'x', value2: 'y' });
console.time('redactAttributes execution time'); console.time('redactAttributes execution time');
expect( expect(
redactAttributes({ redactPaths({
object: { object: {
accounts: { accounts: {
'2e937c05-657c-4de9-8fb3-0813a2245f26': { '2e937c05-657c-4de9-8fb3-0813a2245f26': {
@ -117,6 +111,7 @@ describe('redactAttributes', () => {
hasError: false, hasError: false,
holdings: { holdings: {
'AAPL.US': { 'AAPL.US': {
activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -136,7 +131,6 @@ describe('redactAttributes', () => {
marketPrice: 220.79, marketPrice: 220.79,
symbol: 'AAPL.US', symbol: 'AAPL.US',
tags: [], tags: [],
transactionCount: 1,
allocationInPercentage: 0.044900865255793135, allocationInPercentage: 0.044900865255793135,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
@ -169,6 +163,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.0694356974830054 valueInPercentage: 0.0694356974830054
}, },
'ALV.DE': { 'ALV.DE': {
activitiesCount: 2,
currency: 'EUR', currency: 'EUR',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -188,7 +183,6 @@ describe('redactAttributes', () => {
marketPrice: 296.5, marketPrice: 296.5,
symbol: 'ALV.DE', symbol: 'ALV.DE',
tags: [], tags: [],
transactionCount: 2,
allocationInPercentage: 0.026912563036519527, allocationInPercentage: 0.026912563036519527,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
@ -216,6 +210,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.04161818652826481 valueInPercentage: 0.04161818652826481
}, },
AMZN: { AMZN: {
activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -235,7 +230,6 @@ describe('redactAttributes', () => {
marketPrice: 187.99, marketPrice: 187.99,
symbol: 'AMZN', symbol: 'AMZN',
tags: [], tags: [],
transactionCount: 1,
allocationInPercentage: 0.07646101417126275, allocationInPercentage: 0.07646101417126275,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
@ -268,6 +262,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.11824101426541227 valueInPercentage: 0.11824101426541227
}, },
bitcoin: { bitcoin: {
activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
UNKNOWN: 36985.0332704, UNKNOWN: 36985.0332704,
@ -293,7 +288,6 @@ describe('redactAttributes', () => {
userId: null userId: null
} }
], ],
transactionCount: 1,
allocationInPercentage: 0.15042891393226654, allocationInPercentage: 0.15042891393226654,
assetClass: 'LIQUIDITY', assetClass: 'LIQUIDITY',
assetSubClass: 'CRYPTOCURRENCY', assetSubClass: 'CRYPTOCURRENCY',
@ -319,6 +313,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.232626620912395 valueInPercentage: 0.232626620912395
}, },
BONDORA_GO_AND_GROW: { BONDORA_GO_AND_GROW: {
activitiesCount: 5,
currency: 'EUR', currency: 'EUR',
markets: { markets: {
UNKNOWN: 2231.644722160232, UNKNOWN: 2231.644722160232,
@ -344,7 +339,6 @@ describe('redactAttributes', () => {
userId: null userId: null
} }
], ],
transactionCount: 5,
allocationInPercentage: 0.009076749759365777, allocationInPercentage: 0.009076749759365777,
assetClass: 'FIXED_INCOME', assetClass: 'FIXED_INCOME',
assetSubClass: 'BOND', assetSubClass: 'BOND',
@ -370,6 +364,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.014036487867880205 valueInPercentage: 0.014036487867880205
}, },
FRANKLY95P: { FRANKLY95P: {
activitiesCount: 6,
currency: 'CHF', currency: 'CHF',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -395,7 +390,6 @@ describe('redactAttributes', () => {
userId: null userId: null
} }
], ],
transactionCount: 6,
allocationInPercentage: 0.09095764645669335, allocationInPercentage: 0.09095764645669335,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
@ -494,6 +488,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.14065892911313693 valueInPercentage: 0.14065892911313693
}, },
MSFT: { MSFT: {
activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -513,7 +508,6 @@ describe('redactAttributes', () => {
marketPrice: 428.02, marketPrice: 428.02,
symbol: 'MSFT', symbol: 'MSFT',
tags: [], tags: [],
transactionCount: 1,
allocationInPercentage: 0.05222646409742627, allocationInPercentage: 0.05222646409742627,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
@ -546,6 +540,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.08076416659271518 valueInPercentage: 0.08076416659271518
}, },
TSLA: { TSLA: {
activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -565,7 +560,6 @@ describe('redactAttributes', () => {
marketPrice: 260.46, marketPrice: 260.46,
symbol: 'TSLA', symbol: 'TSLA',
tags: [], tags: [],
transactionCount: 1,
allocationInPercentage: 0.1589050142378352, allocationInPercentage: 0.1589050142378352,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
@ -598,6 +592,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.2457342510950259 valueInPercentage: 0.2457342510950259
}, },
VTI: { VTI: {
activitiesCount: 5,
currency: 'USD', currency: 'USD',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -617,7 +612,6 @@ describe('redactAttributes', () => {
marketPrice: 282.05, marketPrice: 282.05,
symbol: 'VTI', symbol: 'VTI',
tags: [], tags: [],
transactionCount: 5,
allocationInPercentage: 0.057358979326040366, allocationInPercentage: 0.057358979326040366,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
@ -770,6 +764,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.08870120238725339 valueInPercentage: 0.08870120238725339
}, },
'VWRL.SW': { 'VWRL.SW': {
activitiesCount: 5,
currency: 'CHF', currency: 'CHF',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -789,7 +784,6 @@ describe('redactAttributes', () => {
marketPrice: 117.62, marketPrice: 117.62,
symbol: 'VWRL.SW', symbol: 'VWRL.SW',
tags: [], tags: [],
transactionCount: 5,
allocationInPercentage: 0.09386983901959013, allocationInPercentage: 0.09386983901959013,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
@ -1178,6 +1172,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.145162408515095 valueInPercentage: 0.145162408515095
}, },
'XDWD.DE': { 'XDWD.DE': {
activitiesCount: 1,
currency: 'EUR', currency: 'EUR',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -1197,7 +1192,6 @@ describe('redactAttributes', () => {
marketPrice: 105.72, marketPrice: 105.72,
symbol: 'XDWD.DE', symbol: 'XDWD.DE',
tags: [], tags: [],
transactionCount: 1,
allocationInPercentage: 0.03598477442100562, allocationInPercentage: 0.03598477442100562,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
@ -1456,6 +1450,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.055647656152211074 valueInPercentage: 0.055647656152211074
}, },
USD: { USD: {
activitiesCount: 0,
currency: 'USD', currency: 'USD',
allocationInPercentage: 0.20291717628620132, allocationInPercentage: 0.20291717628620132,
assetClass: 'LIQUIDITY', assetClass: 'LIQUIDITY',
@ -1478,7 +1473,6 @@ describe('redactAttributes', () => {
sectors: [], sectors: [],
symbol: 'USD', symbol: 'USD',
tags: [], tags: [],
transactionCount: 0,
valueInBaseCurrency: 49890, valueInBaseCurrency: 49890,
valueInPercentage: 0.3137956381563603 valueInPercentage: 0.3137956381563603
} }
@ -1564,34 +1558,7 @@ describe('redactAttributes', () => {
currentNetWorth: null currentNetWorth: null
} }
}, },
options: [ paths: DEFAULT_REDACTED_PATHS
'balance',
'balanceInBaseCurrency',
'comment',
'convertedBalance',
'dividendInBaseCurrency',
'fee',
'feeInBaseCurrency',
'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'investment',
'netPerformance',
'netPerformanceWithCurrencyEffect',
'quantity',
'symbolMapping',
'totalBalanceInBaseCurrency',
'totalValueInBaseCurrency',
'unitPrice',
'value',
'valueInBaseCurrency'
].map((attribute) => {
return {
attribute,
valueMap: {
'*': null
}
};
})
}) })
).toStrictEqual({ ).toStrictEqual({
accounts: { accounts: {
@ -1648,6 +1615,7 @@ describe('redactAttributes', () => {
hasError: false, hasError: false,
holdings: { holdings: {
'AAPL.US': { 'AAPL.US': {
activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -1667,7 +1635,6 @@ describe('redactAttributes', () => {
marketPrice: 220.79, marketPrice: 220.79,
symbol: 'AAPL.US', symbol: 'AAPL.US',
tags: [], tags: [],
transactionCount: 1,
allocationInPercentage: 0.044900865255793135, allocationInPercentage: 0.044900865255793135,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
@ -1681,7 +1648,7 @@ describe('redactAttributes', () => {
], ],
dataSource: 'EOD_HISTORICAL_DATA', dataSource: 'EOD_HISTORICAL_DATA',
dateOfFirstActivity: '2021-11-30T23:00:00.000Z', dateOfFirstActivity: '2021-11-30T23:00:00.000Z',
dividend: 0, dividend: null,
grossPerformance: null, grossPerformance: null,
grossPerformancePercent: 0.3183066634822068, grossPerformancePercent: 0.3183066634822068,
grossPerformancePercentWithCurrencyEffect: 0.3183066634822068, grossPerformancePercentWithCurrencyEffect: 0.3183066634822068,
@ -1700,6 +1667,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.0694356974830054 valueInPercentage: 0.0694356974830054
}, },
'ALV.DE': { 'ALV.DE': {
activitiesCount: 2,
currency: 'EUR', currency: 'EUR',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -1719,7 +1687,6 @@ describe('redactAttributes', () => {
marketPrice: 296.5, marketPrice: 296.5,
symbol: 'ALV.DE', symbol: 'ALV.DE',
tags: [], tags: [],
transactionCount: 2,
allocationInPercentage: 0.026912563036519527, allocationInPercentage: 0.026912563036519527,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
@ -1728,7 +1695,7 @@ describe('redactAttributes', () => {
], ],
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2021-04-22T22:00:00.000Z', dateOfFirstActivity: '2021-04-22T22:00:00.000Z',
dividend: 192, dividend: null,
grossPerformance: null, grossPerformance: null,
grossPerformancePercent: 0.3719230057375532, grossPerformancePercent: 0.3719230057375532,
grossPerformancePercentWithCurrencyEffect: 0.2650716044872953, grossPerformancePercentWithCurrencyEffect: 0.2650716044872953,
@ -1747,6 +1714,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.04161818652826481 valueInPercentage: 0.04161818652826481
}, },
AMZN: { AMZN: {
activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -1766,7 +1734,6 @@ describe('redactAttributes', () => {
marketPrice: 187.99, marketPrice: 187.99,
symbol: 'AMZN', symbol: 'AMZN',
tags: [], tags: [],
transactionCount: 1,
allocationInPercentage: 0.07646101417126275, allocationInPercentage: 0.07646101417126275,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
@ -1780,7 +1747,7 @@ describe('redactAttributes', () => {
], ],
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2018-09-30T22:00:00.000Z', dateOfFirstActivity: '2018-09-30T22:00:00.000Z',
dividend: 0, dividend: null,
grossPerformance: null, grossPerformance: null,
grossPerformancePercent: 0.8594552890963852, grossPerformancePercent: 0.8594552890963852,
grossPerformancePercentWithCurrencyEffect: 0.8594552890963852, grossPerformancePercentWithCurrencyEffect: 0.8594552890963852,
@ -1799,6 +1766,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.11824101426541227 valueInPercentage: 0.11824101426541227
}, },
bitcoin: { bitcoin: {
activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
UNKNOWN: 36985.0332704, UNKNOWN: 36985.0332704,
@ -1824,14 +1792,13 @@ describe('redactAttributes', () => {
userId: null userId: null
} }
], ],
transactionCount: 1,
allocationInPercentage: 0.15042891393226654, allocationInPercentage: 0.15042891393226654,
assetClass: 'LIQUIDITY', assetClass: 'LIQUIDITY',
assetSubClass: 'CRYPTOCURRENCY', assetSubClass: 'CRYPTOCURRENCY',
countries: [], countries: [],
dataSource: 'COINGECKO', dataSource: 'COINGECKO',
dateOfFirstActivity: '2017-08-15T22:00:00.000Z', dateOfFirstActivity: '2017-08-15T22:00:00.000Z',
dividend: 0, dividend: null,
grossPerformance: null, grossPerformance: null,
grossPerformancePercent: 17.4925166352, grossPerformancePercent: 17.4925166352,
grossPerformancePercentWithCurrencyEffect: 17.4925166352, grossPerformancePercentWithCurrencyEffect: 17.4925166352,
@ -1850,6 +1817,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.232626620912395 valueInPercentage: 0.232626620912395
}, },
BONDORA_GO_AND_GROW: { BONDORA_GO_AND_GROW: {
activitiesCount: 5,
currency: 'EUR', currency: 'EUR',
markets: { markets: {
UNKNOWN: 2231.644722160232, UNKNOWN: 2231.644722160232,
@ -1875,14 +1843,13 @@ describe('redactAttributes', () => {
userId: null userId: null
} }
], ],
transactionCount: 5,
allocationInPercentage: 0.009076749759365777, allocationInPercentage: 0.009076749759365777,
assetClass: 'FIXED_INCOME', assetClass: 'FIXED_INCOME',
assetSubClass: 'BOND', assetSubClass: 'BOND',
countries: [], countries: [],
dataSource: 'MANUAL', dataSource: 'MANUAL',
dateOfFirstActivity: '2021-01-31T23:00:00.000Z', dateOfFirstActivity: '2021-01-31T23:00:00.000Z',
dividend: 11.45, dividend: null,
grossPerformance: null, grossPerformance: null,
grossPerformancePercent: 0, grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: -0.06153834320225245, grossPerformancePercentWithCurrencyEffect: -0.06153834320225245,
@ -1901,6 +1868,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.014036487867880205 valueInPercentage: 0.014036487867880205
}, },
FRANKLY95P: { FRANKLY95P: {
activitiesCount: 6,
currency: 'CHF', currency: 'CHF',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -1926,7 +1894,6 @@ describe('redactAttributes', () => {
userId: null userId: null
} }
], ],
transactionCount: 6,
allocationInPercentage: 0.09095764645669335, allocationInPercentage: 0.09095764645669335,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
@ -1986,7 +1953,7 @@ describe('redactAttributes', () => {
], ],
dataSource: 'MANUAL', dataSource: 'MANUAL',
dateOfFirstActivity: '2021-03-31T22:00:00.000Z', dateOfFirstActivity: '2021-03-31T22:00:00.000Z',
dividend: 0, dividend: null,
grossPerformance: null, grossPerformance: null,
grossPerformancePercent: 0.27579517683678895, grossPerformancePercent: 0.27579517683678895,
grossPerformancePercentWithCurrencyEffect: 0.458553421589667, grossPerformancePercentWithCurrencyEffect: 0.458553421589667,
@ -2005,6 +1972,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.14065892911313693 valueInPercentage: 0.14065892911313693
}, },
MSFT: { MSFT: {
activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -2024,7 +1992,6 @@ describe('redactAttributes', () => {
marketPrice: 428.02, marketPrice: 428.02,
symbol: 'MSFT', symbol: 'MSFT',
tags: [], tags: [],
transactionCount: 1,
allocationInPercentage: 0.05222646409742627, allocationInPercentage: 0.05222646409742627,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
@ -2038,7 +2005,7 @@ describe('redactAttributes', () => {
], ],
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2023-01-02T23:00:00.000Z', dateOfFirstActivity: '2023-01-02T23:00:00.000Z',
dividend: 0, dividend: null,
grossPerformance: null, grossPerformance: null,
grossPerformancePercent: 0.7865431171216295, grossPerformancePercent: 0.7865431171216295,
grossPerformancePercentWithCurrencyEffect: 0.7865431171216295, grossPerformancePercentWithCurrencyEffect: 0.7865431171216295,
@ -2057,6 +2024,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.08076416659271518 valueInPercentage: 0.08076416659271518
}, },
TSLA: { TSLA: {
activitiesCount: 1,
currency: 'USD', currency: 'USD',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -2076,7 +2044,6 @@ describe('redactAttributes', () => {
marketPrice: 260.46, marketPrice: 260.46,
symbol: 'TSLA', symbol: 'TSLA',
tags: [], tags: [],
transactionCount: 1,
allocationInPercentage: 0.1589050142378352, allocationInPercentage: 0.1589050142378352,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'STOCK', assetSubClass: 'STOCK',
@ -2090,7 +2057,7 @@ describe('redactAttributes', () => {
], ],
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2017-01-02T23:00:00.000Z', dateOfFirstActivity: '2017-01-02T23:00:00.000Z',
dividend: 0, dividend: null,
grossPerformance: null, grossPerformance: null,
grossPerformancePercent: 17.184314638161936, grossPerformancePercent: 17.184314638161936,
grossPerformancePercentWithCurrencyEffect: 17.184314638161936, grossPerformancePercentWithCurrencyEffect: 17.184314638161936,
@ -2109,6 +2076,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.2457342510950259 valueInPercentage: 0.2457342510950259
}, },
VTI: { VTI: {
activitiesCount: 5,
currency: 'USD', currency: 'USD',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -2128,7 +2096,6 @@ describe('redactAttributes', () => {
marketPrice: 282.05, marketPrice: 282.05,
symbol: 'VTI', symbol: 'VTI',
tags: [], tags: [],
transactionCount: 5,
allocationInPercentage: 0.057358979326040366, allocationInPercentage: 0.057358979326040366,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
@ -2172,7 +2139,7 @@ describe('redactAttributes', () => {
], ],
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2019-02-28T23:00:00.000Z', dateOfFirstActivity: '2019-02-28T23:00:00.000Z',
dividend: 0, dividend: null,
grossPerformance: null, grossPerformance: null,
grossPerformancePercent: 0.8832083851170418, grossPerformancePercent: 0.8832083851170418,
grossPerformancePercentWithCurrencyEffect: 0.8832083851170418, grossPerformancePercentWithCurrencyEffect: 0.8832083851170418,
@ -2281,6 +2248,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.08870120238725339 valueInPercentage: 0.08870120238725339
}, },
'VWRL.SW': { 'VWRL.SW': {
activitiesCount: 5,
currency: 'CHF', currency: 'CHF',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -2300,7 +2268,6 @@ describe('redactAttributes', () => {
marketPrice: 117.62, marketPrice: 117.62,
symbol: 'VWRL.SW', symbol: 'VWRL.SW',
tags: [], tags: [],
transactionCount: 5,
allocationInPercentage: 0.09386983901959013, allocationInPercentage: 0.09386983901959013,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
@ -2567,7 +2534,7 @@ describe('redactAttributes', () => {
], ],
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2018-02-28T23:00:00.000Z', dateOfFirstActivity: '2018-02-28T23:00:00.000Z',
dividend: 0, dividend: null,
grossPerformance: null, grossPerformance: null,
grossPerformancePercent: 0.3683200415015591, grossPerformancePercent: 0.3683200415015591,
grossPerformancePercentWithCurrencyEffect: 0.5806366182968891, grossPerformancePercentWithCurrencyEffect: 0.5806366182968891,
@ -2681,6 +2648,7 @@ describe('redactAttributes', () => {
valueInPercentage: 0.145162408515095 valueInPercentage: 0.145162408515095
}, },
'XDWD.DE': { 'XDWD.DE': {
activitiesCount: 1,
currency: 'EUR', currency: 'EUR',
markets: { markets: {
UNKNOWN: 0, UNKNOWN: 0,
@ -2700,7 +2668,6 @@ describe('redactAttributes', () => {
marketPrice: 105.72, marketPrice: 105.72,
symbol: 'XDWD.DE', symbol: 'XDWD.DE',
tags: [], tags: [],
transactionCount: 1,
allocationInPercentage: 0.03598477442100562, allocationInPercentage: 0.03598477442100562,
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF', assetSubClass: 'ETF',
@ -2846,7 +2813,7 @@ describe('redactAttributes', () => {
], ],
dataSource: 'YAHOO', dataSource: 'YAHOO',
dateOfFirstActivity: '2021-08-18T22:00:00.000Z', dateOfFirstActivity: '2021-08-18T22:00:00.000Z',
dividend: 0, dividend: null,
grossPerformance: null, grossPerformance: null,
grossPerformancePercent: 0.3474381850624522, grossPerformancePercent: 0.3474381850624522,
grossPerformancePercentWithCurrencyEffect: 0.28744846894552306, grossPerformancePercentWithCurrencyEffect: 0.28744846894552306,
@ -2959,12 +2926,13 @@ describe('redactAttributes', () => {
valueInPercentage: 0.055647656152211074 valueInPercentage: 0.055647656152211074
}, },
USD: { USD: {
activitiesCount: 0,
currency: 'USD', currency: 'USD',
allocationInPercentage: 0.20291717628620132, allocationInPercentage: 0.20291717628620132,
assetClass: 'LIQUIDITY', assetClass: 'LIQUIDITY',
assetSubClass: 'CASH', assetSubClass: 'CASH',
countries: [], countries: [],
dividend: 0, dividend: null,
grossPerformance: null, grossPerformance: null,
grossPerformancePercent: 0, grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: 0, grossPerformancePercentWithCurrencyEffect: 0,
@ -2981,7 +2949,6 @@ describe('redactAttributes', () => {
sectors: [], sectors: [],
symbol: 'USD', symbol: 'USD',
tags: [], tags: [],
transactionCount: 0,
valueInBaseCurrency: null, valueInBaseCurrency: null,
valueInPercentage: 0.3137956381563603 valueInPercentage: 0.3137956381563603
} }

67
apps/api/src/helper/object.helper.ts

@ -1,6 +1,6 @@
import { Big } from 'big.js'; import fastRedact from 'fast-redact';
import jsonpath from 'jsonpath'; import jsonpath from 'jsonpath';
import { cloneDeep, isArray, isObject } from 'lodash'; import { cloneDeep, isObject } from 'lodash';
export function hasNotDefinedValuesInObject(aObject: Object): boolean { export function hasNotDefinedValuesInObject(aObject: Object): boolean {
for (const key in aObject) { for (const key in aObject) {
@ -42,60 +42,29 @@ export function query({
return jsonpath.query(object, pathExpression); return jsonpath.query(object, pathExpression);
} }
export function redactAttributes({ export function redactPaths({
isFirstRun = true,
object, object,
options paths,
valueMap
}: { }: {
isFirstRun?: boolean;
object: any; object: any;
options: { attribute: string; valueMap: { [key: string]: any } }[]; paths: fastRedact.RedactOptions['paths'];
valueMap?: { [key: string]: any };
}): any { }): any {
if (!object || !options?.length) { const redact = fastRedact({
return object; paths,
} censor: (value) => {
if (valueMap) {
// Create deep clone if (valueMap[value]) {
const redactedObject = isFirstRun return valueMap[value];
? JSON.parse(JSON.stringify(object))
: object;
for (const option of options) {
if (redactedObject.hasOwnProperty(option.attribute)) {
if (option.valueMap['*'] || option.valueMap['*'] === null) {
redactedObject[option.attribute] = option.valueMap['*'];
} else if (option.valueMap[redactedObject[option.attribute]]) {
redactedObject[option.attribute] =
option.valueMap[redactedObject[option.attribute]];
}
} else { } else {
// If the attribute is not present on the current object, return value;
// check if it exists on any nested objects
for (const property in redactedObject) {
if (isArray(redactedObject[property])) {
redactedObject[property] = redactedObject[property].map(
(currentObject) => {
return redactAttributes({
options,
isFirstRun: false,
object: currentObject
});
}
);
} else if (
isObject(redactedObject[property]) &&
!(redactedObject[property] instanceof Big)
) {
// Recursively call the function on the nested object
redactedObject[property] = redactAttributes({
options,
isFirstRun: false,
object: redactedObject[property]
});
}
} }
} else {
return null;
} }
} }
});
return redactedObject; return JSON.parse(redact(object));
} }

42
apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts

@ -1,5 +1,8 @@
import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; import { redactPaths } from '@ghostfolio/api/helper/object.helper';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import {
DEFAULT_REDACTED_PATHS,
HEADER_KEY_IMPERSONATION
} from '@ghostfolio/common/config';
import { import {
hasReadRestrictedAccessPermission, hasReadRestrictedAccessPermission,
isRestrictedView isRestrictedView
@ -39,40 +42,9 @@ export class RedactValuesInResponseInterceptor<T> implements NestInterceptor<
}) || }) ||
isRestrictedView(user) isRestrictedView(user)
) { ) {
data = redactAttributes({ data = redactPaths({
object: data, object: data,
options: [ paths: DEFAULT_REDACTED_PATHS
'balance',
'balanceInBaseCurrency',
'comment',
'convertedBalance',
'dividendInBaseCurrency',
'fee',
'feeInBaseCurrency',
'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'interestInBaseCurrency',
'investment',
'netPerformance',
'netPerformanceWithCurrencyEffect',
'quantity',
'symbolMapping',
'totalBalanceInBaseCurrency',
'totalDividendInBaseCurrency',
'totalInterestInBaseCurrency',
'totalValueInBaseCurrency',
'unitPrice',
'unitPriceInAssetProfileCurrency',
'value',
'valueInBaseCurrency'
].map((attribute) => {
return {
attribute,
valueMap: {
'*': null
}
};
})
}); });
} }

19
apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts

@ -1,4 +1,4 @@
import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; import { redactPaths } from '@ghostfolio/api/helper/object.helper';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { encodeDataSource } from '@ghostfolio/common/helper'; import { encodeDataSource } from '@ghostfolio/common/helper';
@ -58,13 +58,18 @@ export class TransformDataSourceInResponseInterceptor<
} }
} }
data = redactAttributes({ data = redactPaths({
object: data,
options: [
{
valueMap, valueMap,
attribute: 'dataSource' object: data,
} paths: [
'activities[*].SymbolProfile.dataSource',
'benchmarks[*].dataSource',
'fearAndGreedIndex.CRYPTOCURRENCIES.dataSource',
'fearAndGreedIndex.STOCKS.dataSource',
'holdings[*].dataSource',
'items[*].dataSource',
'SymbolProfile.dataSource',
'watchlist[*].dataSource'
] ]
}); });
} }

33
apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts

@ -135,10 +135,10 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
shortName, shortName,
symbol symbol
}: { }: {
longName: Price['longName']; longName?: Price['longName'];
quoteType: Price['quoteType']; quoteType?: Price['quoteType'];
shortName: Price['shortName']; shortName?: Price['shortName'];
symbol: Price['symbol']; symbol?: Price['symbol'];
}) { }) {
let name = longName; let name = longName;
@ -206,17 +206,26 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
); );
if (['ETF', 'MUTUALFUND'].includes(assetSubClass)) { if (['ETF', 'MUTUALFUND'].includes(assetSubClass)) {
response.sectors = []; response.holdings =
assetProfile.topHoldings?.holdings?.map(
for (const sectorWeighting of assetProfile.topHoldings ({ holdingName, holdingPercent }) => {
?.sectorWeightings ?? []) { return {
for (const [sector, weight] of Object.entries(sectorWeighting)) { name: this.formatName({ longName: holdingName }),
response.sectors.push({ weight: holdingPercent
};
}
) ?? [];
response.sectors = (
assetProfile.topHoldings?.sectorWeightings ?? []
).flatMap((sectorWeighting) => {
return Object.entries(sectorWeighting).map(([sector, weight]) => {
return {
name: this.parseSector(sector), name: this.parseSector(sector),
weight: weight as number weight: weight as number
};
});
}); });
}
}
} else if ( } else if (
assetSubClass === 'STOCK' && assetSubClass === 'STOCK' &&
assetProfile.summaryProfile?.country assetProfile.summaryProfile?.country

8
apps/api/src/services/tag/tag.service.ts

@ -75,12 +75,16 @@ export class TagService {
} }
}); });
return tags.map(({ _count, id, name, userId }) => ({ return tags
.map(({ _count, id, name, userId }) => ({
id, id,
name, name,
userId, userId,
isUsed: _count.activities > 0 isUsed: _count.activities > 0
})); }))
.sort((a, b) => {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
} }
public async getTagsWithActivityCount() { public async getTagsWithActivityCount() {

6
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -80,6 +80,7 @@ import { AccountDetailDialogParams } from './interfaces/interfaces';
export class GfAccountDetailDialogComponent implements OnDestroy, OnInit { export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
public accountBalances: AccountBalancesResponse['balances']; public accountBalances: AccountBalancesResponse['balances'];
public activities: OrderWithAccount[]; public activities: OrderWithAccount[];
public activitiesCount: number;
public balance: number; public balance: number;
public balancePrecision = 2; public balancePrecision = 2;
public currency: string; public currency: string;
@ -100,7 +101,6 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
public sortColumn = 'date'; public sortColumn = 'date';
public sortDirection: SortDirection = 'desc'; public sortDirection: SortDirection = 'desc';
public totalItems: number; public totalItems: number;
public transactionCount: number;
public user: User; public user: User;
public valueInBaseCurrency: number; public valueInBaseCurrency: number;
@ -215,16 +215,17 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe( .subscribe(
({ ({
activitiesCount,
balance, balance,
currency, currency,
dividendInBaseCurrency, dividendInBaseCurrency,
interestInBaseCurrency, interestInBaseCurrency,
name, name,
platform, platform,
transactionCount,
value, value,
valueInBaseCurrency valueInBaseCurrency
}) => { }) => {
this.activitiesCount = activitiesCount;
this.balance = balance; this.balance = balance;
if ( if (
@ -270,7 +271,6 @@ export class GfAccountDetailDialogComponent implements OnDestroy, OnInit {
this.name = name; this.name = name;
this.platformName = platform?.name ?? '-'; this.platformName = platform?.name ?? '-';
this.transactionCount = transactionCount;
this.valueInBaseCurrency = valueInBaseCurrency; this.valueInBaseCurrency = valueInBaseCurrency;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();

4
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -82,7 +82,7 @@
> >
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="transactionCount" <gf-value i18n size="medium" [value]="activitiesCount"
>Activities</gf-value >Activities</gf-value
> >
</div> </div>
@ -102,8 +102,6 @@
<div class="d-none d-sm-block ml-2" i18n>Holdings</div> <div class="d-none d-sm-block ml-2" i18n>Holdings</div>
</ng-template> </ng-template>
<gf-holdings-table <gf-holdings-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[holdings]="holdings" [holdings]="holdings"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"

39
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -63,7 +63,7 @@ import {
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators'; import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { AdminMarketDataService } from './admin-market-data.service'; import { AdminMarketDataService } from './admin-market-data.service';
import { GfAssetProfileDialogComponent } from './asset-profile-dialog/asset-profile-dialog.component'; import { GfAssetProfileDialogComponent } from './asset-profile-dialog/asset-profile-dialog.component';
@ -482,32 +482,27 @@ export class GfAdminMarketDataComponent
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ dataSource, symbol } = {}) => { .subscribe((result) => {
if (dataSource && symbol) { if (!result) {
this.adminService this.router.navigate(['.'], { relativeTo: this.route });
.addAssetProfile({ dataSource, symbol })
.pipe(
switchMap(() => {
this.isLoading = true;
this.changeDetectorRef.markForCheck();
return this.adminService.fetchAdminMarketData({ return;
filters: this.activeFilters, }
take: this.pageSize
});
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe(({ marketData }) => {
this.dataSource = new MatTableDataSource(marketData);
this.dataSource.sort = this.sort;
this.isLoading = false;
this.changeDetectorRef.markForCheck(); const { addAssetProfile, dataSource, symbol } = result;
if (addAssetProfile && dataSource && symbol) {
this.adminService
.addAssetProfile({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.loadData();
}); });
} else {
this.loadData();
} }
this.router.navigate(['.'], { relativeTo: this.route }); this.onOpenAssetProfileDialog({ dataSource, symbol });
}); });
}); });
} }

8
apps/client/src/app/components/admin-market-data/create-asset-profile-dialog/create-asset-profile-dialog.component.ts

@ -101,6 +101,7 @@ export class GfCreateAssetProfileDialogComponent implements OnDestroy, OnInit {
public onSubmit() { public onSubmit() {
if (this.mode === 'auto') { if (this.mode === 'auto') {
this.dialogRef.close({ this.dialogRef.close({
addAssetProfile: true,
dataSource: dataSource:
this.createAssetProfileForm.get('searchSymbol').value.dataSource, this.createAssetProfileForm.get('searchSymbol').value.dataSource,
symbol: this.createAssetProfileForm.get('searchSymbol').value.symbol symbol: this.createAssetProfileForm.get('searchSymbol').value.symbol
@ -127,10 +128,15 @@ export class GfCreateAssetProfileDialogComponent implements OnDestroy, OnInit {
takeUntil(this.unsubscribeSubject) takeUntil(this.unsubscribeSubject)
) )
.subscribe(() => { .subscribe(() => {
this.dialogRef.close(); this.dialogRef.close({
addAssetProfile: false,
dataSource: this.dataSourceForExchangeRates,
symbol: `${DEFAULT_CURRENCY}${currency}`
});
}); });
} else if (this.mode === 'manual') { } else if (this.mode === 'manual') {
this.dialogRef.close({ this.dialogRef.close({
addAssetProfile: true,
dataSource: 'MANUAL', dataSource: 'MANUAL',
symbol: `${this.ghostfolioPrefix}${this.createAssetProfileForm.get('addSymbol').value}` symbol: `${this.ghostfolioPrefix}${this.createAssetProfileForm.get('addSymbol').value}`
}); });

6
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -73,6 +73,7 @@ import { takeUntil } from 'rxjs/operators';
templateUrl: './admin-overview.html' templateUrl: './admin-overview.html'
}) })
export class GfAdminOverviewComponent implements OnDestroy, OnInit { export class GfAdminOverviewComponent implements OnDestroy, OnInit {
public activitiesCount: number;
public couponDuration: StringValue = '14 days'; public couponDuration: StringValue = '14 days';
public coupons: Coupon[]; public coupons: Coupon[];
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
@ -83,7 +84,6 @@ export class GfAdminOverviewComponent implements OnDestroy, OnInit {
public isDataGatheringEnabled: boolean; public isDataGatheringEnabled: boolean;
public permissions = permissions; public permissions = permissions;
public systemMessage: SystemMessage; public systemMessage: SystemMessage;
public transactionCount: number;
public userCount: number; public userCount: number;
public user: User; public user: User;
public version: string; public version: string;
@ -289,12 +289,12 @@ export class GfAdminOverviewComponent implements OnDestroy, OnInit {
this.adminService this.adminService
.fetchAdminData() .fetchAdminData()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ settings, transactionCount, userCount, version }) => { .subscribe(({ activitiesCount, settings, userCount, version }) => {
this.activitiesCount = activitiesCount;
this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? []; this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? [];
this.isDataGatheringEnabled = this.isDataGatheringEnabled =
settings[PROPERTY_IS_DATA_GATHERING_ENABLED] === false ? false : true; settings[PROPERTY_IS_DATA_GATHERING_ENABLED] === false ? false : true;
this.systemMessage = settings[PROPERTY_SYSTEM_MESSAGE] as SystemMessage; this.systemMessage = settings[PROPERTY_SYSTEM_MESSAGE] as SystemMessage;
this.transactionCount = transactionCount;
this.userCount = userCount; this.userCount = userCount;
this.version = version; this.version = version;

6
apps/client/src/app/components/admin-overview/admin-overview.html

@ -20,11 +20,11 @@
<div class="w-50"> <div class="w-50">
<gf-value <gf-value
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="transactionCount" [value]="activitiesCount"
/> />
@if (transactionCount && userCount) { @if (activitiesCount && userCount) {
<div> <div>
{{ transactionCount / userCount | number: '1.2-2' }} {{ activitiesCount / userCount | number: '1.2-2' }}
<span i18n>per User</span> <span i18n>per User</span>
</div> </div>
} }

38
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -57,7 +57,7 @@ import {
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { switchMap, takeUntil, tap } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -139,19 +139,10 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
]; ];
} }
this.route.paramMap
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
const userId = params.get('userId');
if (userId) {
this.openUserDetailDialog(userId);
}
});
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(
.subscribe((state) => { takeUntil(this.unsubscribeSubject),
tap((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -164,6 +155,15 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
permissions.impersonateAllUsers permissions.impersonateAllUsers
); );
} }
}),
switchMap(() => this.route.paramMap)
)
.subscribe((params) => {
const userId = params.get('userId');
if (userId) {
this.openUserDetailDialog(userId);
}
}); });
addIcons({ addIcons({
@ -208,10 +208,13 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
.deleteUser(aId) .deleteUser(aId)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => { .subscribe(() => {
this.fetchUsers(); this.router.navigate(['..'], { relativeTo: this.route });
}); });
}, },
confirmType: ConfirmationDialogType.Warn, confirmType: ConfirmationDialogType.Warn,
discardFn: () => {
this.router.navigate(['..'], { relativeTo: this.route });
},
title: $localize`Do you really want to delete this user?` title: $localize`Do you really want to delete this user?`
}); });
} }
@ -293,6 +296,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
>(GfUserDetailDialogComponent, { >(GfUserDetailDialogComponent, {
autoFocus: false, autoFocus: false,
data: { data: {
currentUserId: this.user?.id,
deviceType: this.deviceType, deviceType: this.deviceType,
hasPermissionForSubscription: this.hasPermissionForSubscription, hasPermissionForSubscription: this.hasPermissionForSubscription,
locale: this.user?.settings?.locale, locale: this.user?.settings?.locale,
@ -305,10 +309,14 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => { .subscribe((data) => {
if (data?.action === 'delete' && data?.userId) {
this.onDeleteUser(data.userId);
} else {
this.router.navigate( this.router.navigate(
internalRoutes.adminControl.subRoutes.users.routerLink internalRoutes.adminControl.subRoutes.users.routerLink
); );
}
}); });
} }
} }

20
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts

@ -1,6 +1,5 @@
import { import {
getTooltipOptions, getTooltipOptions,
getTooltipPositionerMapTop,
getVerticalHoverLinePlugin getVerticalHoverLinePlugin
} from '@ghostfolio/common/chart-helper'; } from '@ghostfolio/common/chart-helper';
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
@ -15,12 +14,14 @@ import { LineChartItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
import { registerChartConfiguration } from '@ghostfolio/ui/chart';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
type ElementRef,
EventEmitter, EventEmitter,
Input, Input,
OnChanges, OnChanges,
@ -42,7 +43,7 @@ import {
PointElement, PointElement,
TimeScale, TimeScale,
Tooltip, Tooltip,
TooltipPosition type TooltipOptions
} from 'chart.js'; } from 'chart.js';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation'; import annotationPlugin from 'chartjs-plugin-annotation';
@ -78,7 +79,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
@Output() benchmarkChanged = new EventEmitter<string>(); @Output() benchmarkChanged = new EventEmitter<string>();
@ViewChild('chartCanvas') chartCanvas; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'line'>; public chart: Chart<'line'>;
public hasPermissionToAccessAdminControl: boolean; public hasPermissionToAccessAdminControl: boolean;
@ -96,8 +97,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
Tooltip Tooltip
); );
Tooltip.positioners['top'] = (_elements, position: TooltipPosition) => registerChartConfiguration();
getTooltipPositionerMapTop(this.chart, position);
addIcons({ arrowForwardOutline }); addIcons({ arrowForwardOutline });
} }
@ -157,8 +157,10 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
if (this.chartCanvas) { if (this.chartCanvas) {
if (this.chart) { if (this.chart) {
this.chart.data = data; this.chart.data = data;
this.chart.options.plugins ??= {};
this.chart.options.plugins.tooltip = this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration() as unknown; this.getTooltipPluginConfiguration();
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart(this.chartCanvas.nativeElement, { this.chart = new Chart(this.chartCanvas.nativeElement, {
@ -196,7 +198,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
verticalHoverLine: { verticalHoverLine: {
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`
} }
} as unknown, },
responsive: true, responsive: true,
scales: { scales: {
x: { x: {
@ -253,7 +255,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
} }
} }
private getTooltipPluginConfiguration() { private getTooltipPluginConfiguration(): Partial<TooltipOptions<'line'>> {
return { return {
...getTooltipOptions({ ...getTooltipOptions({
colorScheme: this.colorScheme, colorScheme: this.colorScheme,
@ -261,7 +263,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
unit: '%' unit: '%'
}), }),
mode: 'index', mode: 'index',
position: 'top' as unknown, position: 'top',
xAlign: 'center', xAlign: 'center',
yAlign: 'bottom' yAlign: 'bottom'
}; };

14
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -116,11 +116,11 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
}; };
public dataProviderInfo: DataProviderInfo; public dataProviderInfo: DataProviderInfo;
public dataSource: MatTableDataSource<Activity>; public dataSource: MatTableDataSource<Activity>;
public dateOfFirstActivity: string;
public dividendInBaseCurrency: number; public dividendInBaseCurrency: number;
public dividendInBaseCurrencyPrecision = 2; public dividendInBaseCurrencyPrecision = 2;
public dividendYieldPercentWithCurrencyEffect: number; public dividendYieldPercentWithCurrencyEffect: number;
public feeInBaseCurrency: number; public feeInBaseCurrency: number;
public firstBuyDate: string;
public hasPermissionToCreateOwnTag: boolean; public hasPermissionToCreateOwnTag: boolean;
public hasPermissionToReadMarketDataOfOwnAssetProfile: boolean; public hasPermissionToReadMarketDataOfOwnAssetProfile: boolean;
public historicalDataItems: LineChartItem[]; public historicalDataItems: LineChartItem[];
@ -267,10 +267,10 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
activitiesCount, activitiesCount,
averagePrice, averagePrice,
dataProviderInfo, dataProviderInfo,
dateOfFirstActivity,
dividendInBaseCurrency, dividendInBaseCurrency,
dividendYieldPercentWithCurrencyEffect, dividendYieldPercentWithCurrencyEffect,
feeInBaseCurrency, feeInBaseCurrency,
firstBuyDate,
historicalData, historicalData,
investmentInBaseCurrencyWithCurrencyEffect, investmentInBaseCurrencyWithCurrencyEffect,
marketPrice, marketPrice,
@ -298,6 +298,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
this.benchmarkDataItems = []; this.benchmarkDataItems = [];
this.countries = {}; this.countries = {};
this.dataProviderInfo = dataProviderInfo; this.dataProviderInfo = dataProviderInfo;
this.dateOfFirstActivity = dateOfFirstActivity;
this.dividendInBaseCurrency = dividendInBaseCurrency; this.dividendInBaseCurrency = dividendInBaseCurrency;
if ( if (
@ -312,7 +313,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
dividendYieldPercentWithCurrencyEffect; dividendYieldPercentWithCurrencyEffect;
this.feeInBaseCurrency = feeInBaseCurrency; this.feeInBaseCurrency = feeInBaseCurrency;
this.firstBuyDate = firstBuyDate;
this.hasPermissionToReadMarketDataOfOwnAssetProfile = this.hasPermissionToReadMarketDataOfOwnAssetProfile =
hasPermission( hasPermission(
@ -461,16 +461,16 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
} }
} }
if (isToday(parseISO(this.firstBuyDate))) { if (isToday(parseISO(this.dateOfFirstActivity))) {
// Add average price // Add average price
this.historicalDataItems.push({ this.historicalDataItems.push({
date: this.firstBuyDate, date: this.dateOfFirstActivity,
value: this.averagePrice value: this.averagePrice
}); });
// Add benchmark 1 // Add benchmark 1
this.benchmarkDataItems.push({ this.benchmarkDataItems.push({
date: this.firstBuyDate, date: this.dateOfFirstActivity,
value: averagePrice value: averagePrice
}); });
@ -501,7 +501,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
if ( if (
this.benchmarkDataItems[0]?.value === undefined && this.benchmarkDataItems[0]?.value === undefined &&
isSameMonth(parseISO(this.firstBuyDate), new Date()) isSameMonth(parseISO(this.dateOfFirstActivity), new Date())
) { ) {
this.benchmarkDataItems[0].value = this.averagePrice; this.benchmarkDataItems[0].value = this.averagePrice;
} }

7
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html

@ -215,7 +215,7 @@
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[isDate]="true" [isDate]="true"
[locale]="data.locale" [locale]="data.locale"
[value]="firstBuyDate" [value]="dateOfFirstActivity"
>First Activity</gf-value >First Activity</gf-value
> >
</div> </div>
@ -377,13 +377,12 @@
<gf-accounts-table <gf-accounts-table
[accounts]="accounts" [accounts]="accounts"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActivitiesCount]="false"
[showAllocationInPercentage]="user?.settings?.isExperimentalFeatures" [showAllocationInPercentage]="user?.settings?.isExperimentalFeatures"
[showBalance]="false" [showBalance]="false"
[showFooter]="false" [showFooter]="false"
[showTransactions]="false"
[showValue]="false" [showValue]="false"
[showValueInBaseCurrency]="false" [showValueInBaseCurrency]="false"
/> />
@ -400,7 +399,7 @@
<gf-historical-market-data-editor <gf-historical-market-data-editor
[currency]="SymbolProfile?.currency" [currency]="SymbolProfile?.currency"
[dataSource]="SymbolProfile?.dataSource" [dataSource]="SymbolProfile?.dataSource"
[dateOfFirstActivity]="firstBuyDate" [dateOfFirstActivity]="dateOfFirstActivity"
[locale]="data.locale" [locale]="data.locale"
[marketData]="marketDataItems" [marketData]="marketDataItems"
[symbol]="SymbolProfile?.symbol" [symbol]="SymbolProfile?.symbol"

2
apps/client/src/app/components/home-holdings/home-holdings.html

@ -46,8 +46,6 @@
} }
<div [ngClass]="{ 'd-none': viewModeFormControl.value !== 'TABLE' }"> <div [ngClass]="{ 'd-none': viewModeFormControl.value !== 'TABLE' }">
<gf-holdings-table <gf-holdings-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasPermissionToShowQuantities]="false" [hasPermissionToShowQuantities]="false"
[holdings]="holdings" [holdings]="holdings"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"

50
apps/client/src/app/components/investment-chart/investment-chart.component.ts

@ -1,6 +1,5 @@
import { import {
getTooltipOptions, getTooltipOptions,
getTooltipPositionerMapTop,
getVerticalHoverLinePlugin, getVerticalHoverLinePlugin,
transformTickToAbbreviation transformTickToAbbreviation
} from '@ghostfolio/common/chart-helper'; } from '@ghostfolio/common/chart-helper';
@ -15,11 +14,13 @@ import {
import { LineChartItem } from '@ghostfolio/common/interfaces'; import { LineChartItem } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { ColorScheme, GroupBy } from '@ghostfolio/common/types'; import { ColorScheme, GroupBy } from '@ghostfolio/common/types';
import { registerChartConfiguration } from '@ghostfolio/ui/chart';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
type ElementRef,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
@ -34,12 +35,15 @@ import {
LineController, LineController,
LineElement, LineElement,
PointElement, PointElement,
type ScriptableLineSegmentContext,
TimeScale, TimeScale,
Tooltip, Tooltip,
TooltipPosition type TooltipOptions
} from 'chart.js'; } from 'chart.js';
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation'; import annotationPlugin, {
type AnnotationOptions
} from 'chartjs-plugin-annotation';
import { isAfter } from 'date-fns'; import { isAfter } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -62,7 +66,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
@Input() locale = getLocale(); @Input() locale = getLocale();
@Input() savingsRate = 0; @Input() savingsRate = 0;
@ViewChild('chartCanvas') chartCanvas; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'bar' | 'line'>; public chart: Chart<'bar' | 'line'>;
private investments: InvestmentItem[]; private investments: InvestmentItem[];
@ -81,8 +85,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
Tooltip Tooltip
); );
Tooltip.positioners['top'] = (_elements, position: TooltipPosition) => registerChartConfiguration();
getTooltipPositionerMapTop(this.chart, position);
} }
public ngOnChanges() { public ngOnChanges() {
@ -121,12 +124,12 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
}), }),
label: this.benchmarkDataLabel, label: this.benchmarkDataLabel,
segment: { segment: {
borderColor: (context: unknown) => borderColor: (context) =>
this.isInFuture( this.isInFuture(
context, context,
`rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.67)` `rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.67)`
), ),
borderDash: (context: unknown) => this.isInFuture(context, [2, 2]) borderDash: (context) => this.isInFuture(context, [2, 2])
}, },
stepped: true stepped: true
}, },
@ -143,12 +146,12 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
label: $localize`Total Amount`, label: $localize`Total Amount`,
pointRadius: 0, pointRadius: 0,
segment: { segment: {
borderColor: (context: unknown) => borderColor: (context) =>
this.isInFuture( this.isInFuture(
context, context,
`rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.67)` `rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.67)`
), ),
borderDash: (context: unknown) => this.isInFuture(context, [2, 2]) borderDash: (context) => this.isInFuture(context, [2, 2])
} }
} }
] ]
@ -157,17 +160,14 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
if (this.chartCanvas) { if (this.chartCanvas) {
if (this.chart) { if (this.chart) {
this.chart.data = chartData; this.chart.data = chartData;
this.chart.options.plugins ??= {};
this.chart.options.plugins.tooltip = this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration() as unknown; this.getTooltipPluginConfiguration();
if ( const annotations = this.chart.options.plugins.annotation
this.savingsRate && .annotations as Record<string, AnnotationOptions<'line'>>;
// @ts-ignore if (this.savingsRate && annotations.savingsRate) {
this.chart.options.plugins.annotation.annotations.savingsRate annotations.savingsRate.value = this.savingsRate;
) {
// @ts-ignore
this.chart.options.plugins.annotation.annotations.savingsRate.value =
this.savingsRate;
} }
this.chart.update(); this.chart.update();
@ -201,7 +201,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
color: 'white', color: 'white',
content: $localize`Savings Rate`, content: $localize`Savings Rate`,
display: true, display: true,
font: { size: '10px', weight: 'normal' }, font: { size: 10, weight: 'normal' },
padding: { padding: {
x: 4, x: 4,
y: 2 y: 2
@ -229,7 +229,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
verticalHoverLine: { verticalHoverLine: {
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`
} }
} as unknown, },
responsive: true, responsive: true,
scales: { scales: {
x: { x: {
@ -286,7 +286,9 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
} }
} }
private getTooltipPluginConfiguration() { private getTooltipPluginConfiguration(): Partial<
TooltipOptions<'bar' | 'line'>
> {
return { return {
...getTooltipOptions({ ...getTooltipOptions({
colorScheme: this.colorScheme, colorScheme: this.colorScheme,
@ -296,13 +298,13 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
unit: this.isInPercent ? '%' : undefined unit: this.isInPercent ? '%' : undefined
}), }),
mode: 'index', mode: 'index',
position: 'top' as unknown, position: 'top',
xAlign: 'center', xAlign: 'center',
yAlign: 'bottom' yAlign: 'bottom'
}; };
} }
private isInFuture<T>(aContext: any, aValue: T) { private isInFuture<T>(aContext: ScriptableLineSegmentContext, aValue: T) {
return isAfter(new Date(aContext?.p1?.parsed?.x), new Date()) return isAfter(new Date(aContext?.p1?.parsed?.x), new Date())
? aValue ? aValue
: undefined; : undefined;

1
apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts

@ -1,4 +1,5 @@
export interface UserDetailDialogParams { export interface UserDetailDialogParams {
currentUserId: string;
deviceType: string; deviceType: string;
hasPermissionForSubscription: boolean; hasPermissionForSubscription: boolean;
locale: string; locale: string;

25
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts

@ -1,6 +1,4 @@
import { AdminUserResponse } from '@ghostfolio/common/interfaces'; import { AdminUserResponse } from '@ghostfolio/common/interfaces';
import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer';
import { GfDialogHeaderComponent } from '@ghostfolio/ui/dialog-header';
import { AdminService } from '@ghostfolio/ui/services'; import { AdminService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
@ -16,6 +14,10 @@ import {
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { ellipsisVertical } from 'ionicons/icons';
import { EMPTY, Subject } from 'rxjs'; import { EMPTY, Subject } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators'; import { catchError, takeUntil } from 'rxjs/operators';
@ -25,11 +27,11 @@ import { UserDetailDialogParams } from './interfaces/interfaces';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' }, host: { class: 'd-flex flex-column h-100' },
imports: [ imports: [
GfDialogFooterComponent,
GfDialogHeaderComponent,
GfValueComponent, GfValueComponent,
IonIcon,
MatButtonModule, MatButtonModule,
MatDialogModule MatDialogModule,
MatMenuModule
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-user-detail-dialog', selector: 'gf-user-detail-dialog',
@ -46,7 +48,11 @@ export class GfUserDetailDialogComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams, @Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams,
public dialogRef: MatDialogRef<GfUserDetailDialogComponent> public dialogRef: MatDialogRef<GfUserDetailDialogComponent>
) {} ) {
addIcons({
ellipsisVertical
});
}
public ngOnInit() { public ngOnInit() {
this.adminService this.adminService
@ -66,6 +72,13 @@ export class GfUserDetailDialogComponent implements OnDestroy, OnInit {
}); });
} }
public deleteUser() {
this.dialogRef.close({
action: 'delete',
userId: this.data.userId
});
}
public onClose() { public onClose() {
this.dialogRef.close(); this.dialogRef.close();
} }

40
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

@ -1,9 +1,28 @@
<gf-dialog-header <div class="d-flex justify-content-end">
position="center" <button
[deviceType]="data.deviceType" class="mx-1 no-min-width px-2"
(closeButtonClicked)="onClose()" mat-button
/> type="button"
[matMenuTriggerFor]="userDetailActionsMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-vertical" />
</button>
<mat-menu
#userDetailActionsMenu="matMenu"
class="no-max-width"
xPosition="before"
>
<button
mat-menu-item
type="button"
[disabled]="this.data.currentUserId === this.data.userId"
(click)="deleteUser()"
>
<ng-container i18n>Delete</ng-container>
</button>
</mat-menu>
</div>
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<div class="container p-0"> <div class="container p-0">
<div class="mb-3 row"> <div class="mb-3 row">
@ -103,7 +122,8 @@
</div> </div>
</div> </div>
<gf-dialog-footer <div class="justify-content-end" mat-dialog-actions>
[deviceType]="data.deviceType" <button mat-button type="button" (click)="onClose()">
(closeButtonClicked)="onClose()" <ng-container i18n>Close</ng-container>
/> </button>
</div>

18
apps/client/src/app/pages/about/overview/about-overview-page.html

@ -207,18 +207,20 @@
<div class="col-12"> <div class="col-12">
<h2 class="h4 mb-3">Sponsors</h2> <h2 class="h4 mb-3">Sponsors</h2>
<div class="text-center"> <div class="text-center">
<small>Browser testing via</small> <div class="mb-2 small">Browser testing via</div>
<br />
<a <a
href="https://www.lambdatest.com?utm_medium=sponsor&utm_source=ghostfolio" href="https://www.testmuai.com?utm_medium=sponsor&utm_source=ghostfolio"
target="_blank" target="_blank"
title="LambdaTest - AI Powered Testing Tool" title="TestMu AI - AI Powered Testing Tool"
> >
<img <img
alt="LambdaTest Logo" alt="TestMu AI Logo"
height="45" height="32"
src="../assets/images/sponsors/logo-lambdatest.png" [src]="
width="250" user?.settings?.colorScheme === 'DARK'
? '../assets/images/sponsors/logo-testmu-light.svg'
: '../assets/images/sponsors/logo-testmu-dark.svg'
"
/> />
</a> </a>
</div> </div>

10
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -38,6 +38,7 @@ import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-ba
}) })
export class GfAccountsPageComponent implements OnDestroy, OnInit { export class GfAccountsPageComponent implements OnDestroy, OnInit {
public accounts: AccountModel[]; public accounts: AccountModel[];
public activitiesCount = 0;
public deviceType: string; public deviceType: string;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionToCreateAccount: boolean; public hasPermissionToCreateAccount: boolean;
@ -45,7 +46,6 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
public routeQueryParams: Subscription; public routeQueryParams: Subscription;
public totalBalanceInBaseCurrency = 0; public totalBalanceInBaseCurrency = 0;
public totalValueInBaseCurrency = 0; public totalValueInBaseCurrency = 0;
public transactionCount = 0;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -128,14 +128,14 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
.subscribe( .subscribe(
({ ({
accounts, accounts,
activitiesCount,
totalBalanceInBaseCurrency, totalBalanceInBaseCurrency,
totalValueInBaseCurrency, totalValueInBaseCurrency
transactionCount
}) => { }) => {
this.accounts = accounts; this.accounts = accounts;
this.activitiesCount = activitiesCount;
this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency; this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency;
this.totalValueInBaseCurrency = totalValueInBaseCurrency; this.totalValueInBaseCurrency = totalValueInBaseCurrency;
this.transactionCount = transactionCount;
if (this.accounts?.length <= 0) { if (this.accounts?.length <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } }); this.router.navigate([], { queryParams: { createDialog: true } });
@ -358,8 +358,8 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
private reset() { private reset() {
this.accounts = undefined; this.accounts = undefined;
this.activitiesCount = 0;
this.totalBalanceInBaseCurrency = 0; this.totalBalanceInBaseCurrency = 0;
this.totalValueInBaseCurrency = 0; this.totalValueInBaseCurrency = 0;
this.transactionCount = 0;
} }
} }

3
apps/client/src/app/pages/accounts/accounts-page.html

@ -4,8 +4,8 @@
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Accounts</h1> <h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Accounts</h1>
<gf-accounts-table <gf-accounts-table
[accounts]="accounts" [accounts]="accounts"
[activitiesCount]="activitiesCount"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]=" [showActions]="
!hasImpersonationId && !hasImpersonationId &&
@ -14,7 +14,6 @@
" "
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency" [totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
[totalValueInBaseCurrency]="totalValueInBaseCurrency" [totalValueInBaseCurrency]="totalValueInBaseCurrency"
[transactionCount]="transactionCount"
(accountDeleted)="onDeleteAccount($event)" (accountDeleted)="onDeleteAccount($event)"
(accountToUpdate)="onUpdateAccount($event)" (accountToUpdate)="onUpdateAccount($event)"
(transferBalance)="onTransferBalance()" (transferBalance)="onTransferBalance()"

11
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -2,6 +2,7 @@ import { GfBenchmarkComparatorComponent } from '@ghostfolio/client/components/be
import { GfInvestmentChartComponent } from '@ghostfolio/client/components/investment-chart/investment-chart.component'; import { GfInvestmentChartComponent } from '@ghostfolio/client/components/investment-chart/investment-chart.component';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config';
import { import {
HistoricalDataItem, HistoricalDataItem,
InvestmentItem, InvestmentItem,
@ -94,6 +95,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
public performanceDataItems: HistoricalDataItem[]; public performanceDataItems: HistoricalDataItem[];
public performanceDataItemsInPercentage: HistoricalDataItem[]; public performanceDataItemsInPercentage: HistoricalDataItem[];
public portfolioEvolutionDataLabel = $localize`Investment`; public portfolioEvolutionDataLabel = $localize`Investment`;
public precision = 2;
public streaks: PortfolioInvestmentsResponse['streaks']; public streaks: PortfolioInvestmentsResponse['streaks'];
public top3: PortfolioPosition[]; public top3: PortfolioPosition[];
public unitCurrentStreak: string; public unitCurrentStreak: string;
@ -317,12 +319,21 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
: valueInPercentage : valueInPercentage
}); });
} }
this.performanceDataItemsInPercentage.push({ this.performanceDataItemsInPercentage.push({
date, date,
value: netPerformanceInPercentageWithCurrencyEffect value: netPerformanceInPercentageWithCurrencyEffect
}); });
} }
if (
this.deviceType === 'mobile' &&
this.performance.currentValueInBaseCurrency >=
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES
) {
this.precision = 0;
}
this.isLoadingInvestmentChart = false; this.isLoadingInvestmentChart = false;
this.updateBenchmarkDataItems(); this.updateBenchmarkDataItems();

66
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -74,6 +74,72 @@
</div> </div>
</div> </div>
} }
@if (user?.settings?.isExperimentalFeatures) {
<div class="mb-5 row">
<div class="col-lg-4 mb-3 mb-lg-0">
<mat-card appearance="outlined">
<mat-card-content>
<gf-value
i18n
size="large"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[precision]="precision"
[unit]="user?.settings?.baseCurrency"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentValueInBaseCurrency
"
>Total amount</gf-value
>
</mat-card-content>
</mat-card>
</div>
<div class="col-lg-4 mb-3 mb-lg-0">
<mat-card appearance="outlined">
<mat-card-content>
<gf-value
i18n
size="large"
[colorizeSign]="true"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[precision]="precision"
[unit]="user?.settings?.baseCurrency"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.netPerformanceWithCurrencyEffect
"
>Change with currency effect</gf-value
>
</mat-card-content>
</mat-card>
</div>
<div class="col-lg-4">
<mat-card appearance="outlined">
<mat-card-content>
<gf-value
i18n
size="large"
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.netPerformancePercentageWithCurrencyEffect
"
>Performance with currency effect</gf-value
>
</mat-card-content>
</mat-card>
</div>
</div>
}
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col-lg"> <div class="col-lg">
<gf-benchmark-comparator <gf-benchmark-comparator

1
apps/client/src/app/pages/pricing/pricing-page.component.ts

@ -82,6 +82,7 @@ export class GfPricingPageComponent implements OnDestroy, OnInit {
'frankly', 'frankly',
'Interactive Brokers', 'Interactive Brokers',
'Mintos', 'Mintos',
'Monefit SmartSaver',
'Swissquote', 'Swissquote',
'VIAC', 'VIAC',
'Zak' 'Zak'

9
apps/client/src/app/pages/pricing/pricing-page.html

@ -195,12 +195,9 @@
</p> </p>
<ul class="list-unstyled mb-3"> <ul class="list-unstyled mb-3">
<li class="mb-2"> <li class="mb-2">
<span <ng-container i18n
><ng-container i18n>Everything in</ng-container> >Everything in <strong>Basic</strong>, plus</ng-container
<ng-container>&nbsp;</ng-container> >:
<strong>Basic</strong>,
<ng-container i18n>plus</ng-container>:</span
>
</li> </li>
<li class="align-items-center d-flex mb-1"> <li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline" /> <ion-icon class="mr-1" name="checkmark-circle-outline" />

1
apps/client/src/app/pages/public/public-page.html

@ -78,7 +78,6 @@
[showLabels]="deviceType !== 'mobile'" [showLabels]="deviceType !== 'mobile'"
/> />
<gf-holdings-table <gf-holdings-table
[deviceType]="deviceType"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[hasPermissionToShowQuantities]="false" [hasPermissionToShowQuantities]="false"
[hasPermissionToShowValues]="false" [hasPermissionToShowValues]="false"

BIN
apps/client/src/assets/images/sponsors/logo-lambdatest.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

33
apps/client/src/assets/images/sponsors/logo-testmu-dark.svg

@ -0,0 +1,33 @@
<svg width="392" height="61" viewBox="0 0 392 61" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M53.6262 0.138739H37.5382V39.0182H53.6262V0.138739Z" fill="black" style="fill:black;fill-opacity:1;"/>
<path d="M16.0881 0.138729H1.90735e-05V22.2598L16.0881 39.0182V0.138729Z" fill="black" style="fill:black;fill-opacity:1;"/>
<path d="M16.0881 39.0182H1.90735e-05V60.4689H16.0881V39.0182Z" fill="black" style="fill:black;fill-opacity:1;"/>
<path d="M37.5391 39.0181H16.0883L28.8247 55.1062H37.5391V39.0181Z" fill="black" style="fill:black;fill-opacity:1;"/>
<mask id="path-5-outside-1_344_209" maskUnits="userSpaceOnUse" x="67.6262" y="-0.6" width="324" height="56" fill="black">
<rect fill="white" x="67.6262" y="-0.6" width="324" height="56"/>
<path d="M69.6262 1.4H109.66V7.32H92.8622V53.2H86.4242V7.32H69.6262V1.4Z"/>
<path d="M107.66 34.33C107.66 30.482 108.4 27.1027 109.88 24.192C111.41 21.2813 113.531 19.0367 116.244 17.458C118.958 15.83 122.066 15.016 125.568 15.016C129.022 15.016 132.08 15.7313 134.744 17.162C137.408 18.5927 139.505 20.6647 141.034 23.378C142.564 26.0913 143.378 29.2733 143.476 32.924C143.476 33.4667 143.427 34.33 143.328 35.514H114.024V36.032C114.123 39.732 115.258 42.692 117.428 44.912C119.599 47.132 122.436 48.242 125.938 48.242C128.652 48.242 130.946 47.576 132.82 46.244C134.744 44.8627 136.027 42.9633 136.668 40.546H142.81C142.07 44.394 140.22 47.5513 137.26 50.018C134.3 52.4353 130.674 53.644 126.382 53.644C122.633 53.644 119.352 52.8547 116.54 51.276C113.728 49.648 111.533 47.3787 109.954 44.468C108.425 41.508 107.66 38.1287 107.66 34.33ZM137.186 30.482C136.89 27.2753 135.682 24.784 133.56 23.008C131.488 21.232 128.849 20.344 125.642 20.344C122.781 20.344 120.265 21.2813 118.094 23.156C115.924 25.0307 114.69 27.4727 114.394 30.482H137.186Z"/>
<path d="M154.992 40.768C155.091 43.0373 156.077 44.8873 157.952 46.318C159.876 47.7487 162.343 48.464 165.352 48.464C168.016 48.464 170.162 47.946 171.79 46.91C173.467 45.8247 174.306 44.4187 174.306 42.692C174.306 41.212 173.911 40.0773 173.122 39.288C172.333 38.4987 171.297 37.956 170.014 37.66C168.781 37.364 167.005 37.068 164.686 36.772C161.43 36.3773 158.766 35.8593 156.694 35.218C154.622 34.5767 152.945 33.5653 151.662 32.184C150.379 30.7533 149.738 28.8047 149.738 26.338C149.738 24.118 150.355 22.1447 151.588 20.418C152.871 18.6913 154.597 17.3593 156.768 16.422C158.939 15.4353 161.381 14.942 164.094 14.942C168.534 14.942 172.135 15.978 174.898 18.05C177.71 20.122 179.264 23.082 179.56 26.93H173.492C173.245 24.9567 172.283 23.3287 170.606 22.046C168.929 20.7633 166.832 20.122 164.316 20.122C161.751 20.122 159.679 20.6647 158.1 21.75C156.521 22.786 155.732 24.1673 155.732 25.894C155.732 27.1767 156.102 28.1633 156.842 28.854C157.582 29.5447 158.519 30.0133 159.654 30.26C160.838 30.5067 162.614 30.778 164.982 31.074C168.287 31.4687 171.001 32.0113 173.122 32.702C175.293 33.3927 177.019 34.5027 178.302 36.032C179.634 37.5613 180.3 39.6333 180.3 42.248C180.3 44.5173 179.634 46.5153 178.302 48.242C176.97 49.9687 175.169 51.3007 172.9 52.238C170.631 53.1753 168.115 53.644 165.352 53.644C160.419 53.644 156.447 52.5093 153.438 50.24C150.478 47.9707 148.973 44.8133 148.924 40.768H154.992Z"/>
<path d="M189.7 21.01H182.892V15.46H189.7V4.878H195.768V15.46H205.314V21.01H195.768V43.284C195.768 44.8627 196.064 45.9973 196.656 46.688C197.297 47.3293 198.383 47.65 199.912 47.65H206.498V53.2H199.542C195.99 53.2 193.449 52.4107 191.92 50.832C190.44 49.2533 189.7 46.762 189.7 43.358V21.01Z"/>
<path d="M211.703 1.4H219.843L238.195 44.394L256.325 1.4H264.317V53.2H258.027V12.352L240.859 53.2H235.161L217.993 12.352V53.2H211.703V1.4Z"/>
<path d="M305.955 15.46V53.2H300.701L299.887 48.168C297.026 51.8187 293.005 53.644 287.825 53.644C283.336 53.644 279.734 52.2627 277.021 49.5C274.308 46.7373 272.951 42.396 272.951 36.476V15.46H279.019V36.106C279.019 40.0033 279.858 42.988 281.535 45.06C283.262 47.0827 285.704 48.094 288.861 48.094C292.314 48.094 295.003 46.8607 296.927 44.394C298.9 41.9273 299.887 38.5727 299.887 34.33V15.46H305.955Z"/>
<path d="M330.264 53.2L350.022 1.4H357.644L377.328 53.2H370.446L364.97 39.214H342.4L336.924 53.2H330.264ZM344.324 33.368H363.046L353.648 8.43L344.324 33.368Z"/>
<path d="M383.189 1.4H389.627V53.2H383.189V1.4Z"/>
</mask>
<path d="M69.6262 1.4H109.66V7.32H92.8622V53.2H86.4242V7.32H69.6262V1.4Z" fill="black" style="fill:black;fill-opacity:1;"/>
<path d="M107.66 34.33C107.66 30.482 108.4 27.1027 109.88 24.192C111.41 21.2813 113.531 19.0367 116.244 17.458C118.958 15.83 122.066 15.016 125.568 15.016C129.022 15.016 132.08 15.7313 134.744 17.162C137.408 18.5927 139.505 20.6647 141.034 23.378C142.564 26.0913 143.378 29.2733 143.476 32.924C143.476 33.4667 143.427 34.33 143.328 35.514H114.024V36.032C114.123 39.732 115.258 42.692 117.428 44.912C119.599 47.132 122.436 48.242 125.938 48.242C128.652 48.242 130.946 47.576 132.82 46.244C134.744 44.8627 136.027 42.9633 136.668 40.546H142.81C142.07 44.394 140.22 47.5513 137.26 50.018C134.3 52.4353 130.674 53.644 126.382 53.644C122.633 53.644 119.352 52.8547 116.54 51.276C113.728 49.648 111.533 47.3787 109.954 44.468C108.425 41.508 107.66 38.1287 107.66 34.33ZM137.186 30.482C136.89 27.2753 135.682 24.784 133.56 23.008C131.488 21.232 128.849 20.344 125.642 20.344C122.781 20.344 120.265 21.2813 118.094 23.156C115.924 25.0307 114.69 27.4727 114.394 30.482H137.186Z" fill="black" style="fill:black;fill-opacity:1;"/>
<path d="M154.992 40.768C155.091 43.0373 156.077 44.8873 157.952 46.318C159.876 47.7487 162.343 48.464 165.352 48.464C168.016 48.464 170.162 47.946 171.79 46.91C173.467 45.8247 174.306 44.4187 174.306 42.692C174.306 41.212 173.911 40.0773 173.122 39.288C172.333 38.4987 171.297 37.956 170.014 37.66C168.781 37.364 167.005 37.068 164.686 36.772C161.43 36.3773 158.766 35.8593 156.694 35.218C154.622 34.5767 152.945 33.5653 151.662 32.184C150.379 30.7533 149.738 28.8047 149.738 26.338C149.738 24.118 150.355 22.1447 151.588 20.418C152.871 18.6913 154.597 17.3593 156.768 16.422C158.939 15.4353 161.381 14.942 164.094 14.942C168.534 14.942 172.135 15.978 174.898 18.05C177.71 20.122 179.264 23.082 179.56 26.93H173.492C173.245 24.9567 172.283 23.3287 170.606 22.046C168.929 20.7633 166.832 20.122 164.316 20.122C161.751 20.122 159.679 20.6647 158.1 21.75C156.521 22.786 155.732 24.1673 155.732 25.894C155.732 27.1767 156.102 28.1633 156.842 28.854C157.582 29.5447 158.519 30.0133 159.654 30.26C160.838 30.5067 162.614 30.778 164.982 31.074C168.287 31.4687 171.001 32.0113 173.122 32.702C175.293 33.3927 177.019 34.5027 178.302 36.032C179.634 37.5613 180.3 39.6333 180.3 42.248C180.3 44.5173 179.634 46.5153 178.302 48.242C176.97 49.9687 175.169 51.3007 172.9 52.238C170.631 53.1753 168.115 53.644 165.352 53.644C160.419 53.644 156.447 52.5093 153.438 50.24C150.478 47.9707 148.973 44.8133 148.924 40.768H154.992Z" fill="black" style="fill:black;fill-opacity:1;"/>
<path d="M189.7 21.01H182.892V15.46H189.7V4.878H195.768V15.46H205.314V21.01H195.768V43.284C195.768 44.8627 196.064 45.9973 196.656 46.688C197.297 47.3293 198.383 47.65 199.912 47.65H206.498V53.2H199.542C195.99 53.2 193.449 52.4107 191.92 50.832C190.44 49.2533 189.7 46.762 189.7 43.358V21.01Z" fill="black" style="fill:black;fill-opacity:1;"/>
<path d="M211.703 1.4H219.843L238.195 44.394L256.325 1.4H264.317V53.2H258.027V12.352L240.859 53.2H235.161L217.993 12.352V53.2H211.703V1.4Z" fill="black" style="fill:black;fill-opacity:1;"/>
<path d="M305.955 15.46V53.2H300.701L299.887 48.168C297.026 51.8187 293.005 53.644 287.825 53.644C283.336 53.644 279.734 52.2627 277.021 49.5C274.308 46.7373 272.951 42.396 272.951 36.476V15.46H279.019V36.106C279.019 40.0033 279.858 42.988 281.535 45.06C283.262 47.0827 285.704 48.094 288.861 48.094C292.314 48.094 295.003 46.8607 296.927 44.394C298.9 41.9273 299.887 38.5727 299.887 34.33V15.46H305.955Z" fill="black" style="fill:black;fill-opacity:1;"/>
<path d="M330.264 53.2L350.022 1.4H357.644L377.328 53.2H370.446L364.97 39.214H342.4L336.924 53.2H330.264ZM344.324 33.368H363.046L353.648 8.43L344.324 33.368Z" fill="black" style="fill:black;fill-opacity:1;"/>
<path d="M383.189 1.4H389.627V53.2H383.189V1.4Z" fill="black" style="fill:black;fill-opacity:1;"/>
<path d="M69.6262 1.4H109.66V7.32H92.8622V53.2H86.4242V7.32H69.6262V1.4Z" stroke="black" style="stroke:black;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_344_209)"/>
<path d="M107.66 34.33C107.66 30.482 108.4 27.1027 109.88 24.192C111.41 21.2813 113.531 19.0367 116.244 17.458C118.958 15.83 122.066 15.016 125.568 15.016C129.022 15.016 132.08 15.7313 134.744 17.162C137.408 18.5927 139.505 20.6647 141.034 23.378C142.564 26.0913 143.378 29.2733 143.476 32.924C143.476 33.4667 143.427 34.33 143.328 35.514H114.024V36.032C114.123 39.732 115.258 42.692 117.428 44.912C119.599 47.132 122.436 48.242 125.938 48.242C128.652 48.242 130.946 47.576 132.82 46.244C134.744 44.8627 136.027 42.9633 136.668 40.546H142.81C142.07 44.394 140.22 47.5513 137.26 50.018C134.3 52.4353 130.674 53.644 126.382 53.644C122.633 53.644 119.352 52.8547 116.54 51.276C113.728 49.648 111.533 47.3787 109.954 44.468C108.425 41.508 107.66 38.1287 107.66 34.33ZM137.186 30.482C136.89 27.2753 135.682 24.784 133.56 23.008C131.488 21.232 128.849 20.344 125.642 20.344C122.781 20.344 120.265 21.2813 118.094 23.156C115.924 25.0307 114.69 27.4727 114.394 30.482H137.186Z" stroke="black" style="stroke:black;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_344_209)"/>
<path d="M154.992 40.768C155.091 43.0373 156.077 44.8873 157.952 46.318C159.876 47.7487 162.343 48.464 165.352 48.464C168.016 48.464 170.162 47.946 171.79 46.91C173.467 45.8247 174.306 44.4187 174.306 42.692C174.306 41.212 173.911 40.0773 173.122 39.288C172.333 38.4987 171.297 37.956 170.014 37.66C168.781 37.364 167.005 37.068 164.686 36.772C161.43 36.3773 158.766 35.8593 156.694 35.218C154.622 34.5767 152.945 33.5653 151.662 32.184C150.379 30.7533 149.738 28.8047 149.738 26.338C149.738 24.118 150.355 22.1447 151.588 20.418C152.871 18.6913 154.597 17.3593 156.768 16.422C158.939 15.4353 161.381 14.942 164.094 14.942C168.534 14.942 172.135 15.978 174.898 18.05C177.71 20.122 179.264 23.082 179.56 26.93H173.492C173.245 24.9567 172.283 23.3287 170.606 22.046C168.929 20.7633 166.832 20.122 164.316 20.122C161.751 20.122 159.679 20.6647 158.1 21.75C156.521 22.786 155.732 24.1673 155.732 25.894C155.732 27.1767 156.102 28.1633 156.842 28.854C157.582 29.5447 158.519 30.0133 159.654 30.26C160.838 30.5067 162.614 30.778 164.982 31.074C168.287 31.4687 171.001 32.0113 173.122 32.702C175.293 33.3927 177.019 34.5027 178.302 36.032C179.634 37.5613 180.3 39.6333 180.3 42.248C180.3 44.5173 179.634 46.5153 178.302 48.242C176.97 49.9687 175.169 51.3007 172.9 52.238C170.631 53.1753 168.115 53.644 165.352 53.644C160.419 53.644 156.447 52.5093 153.438 50.24C150.478 47.9707 148.973 44.8133 148.924 40.768H154.992Z" stroke="black" style="stroke:black;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_344_209)"/>
<path d="M189.7 21.01H182.892V15.46H189.7V4.878H195.768V15.46H205.314V21.01H195.768V43.284C195.768 44.8627 196.064 45.9973 196.656 46.688C197.297 47.3293 198.383 47.65 199.912 47.65H206.498V53.2H199.542C195.99 53.2 193.449 52.4107 191.92 50.832C190.44 49.2533 189.7 46.762 189.7 43.358V21.01Z" stroke="black" style="stroke:black;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_344_209)"/>
<path d="M211.703 1.4H219.843L238.195 44.394L256.325 1.4H264.317V53.2H258.027V12.352L240.859 53.2H235.161L217.993 12.352V53.2H211.703V1.4Z" stroke="black" style="stroke:black;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_344_209)"/>
<path d="M305.955 15.46V53.2H300.701L299.887 48.168C297.026 51.8187 293.005 53.644 287.825 53.644C283.336 53.644 279.734 52.2627 277.021 49.5C274.308 46.7373 272.951 42.396 272.951 36.476V15.46H279.019V36.106C279.019 40.0033 279.858 42.988 281.535 45.06C283.262 47.0827 285.704 48.094 288.861 48.094C292.314 48.094 295.003 46.8607 296.927 44.394C298.9 41.9273 299.887 38.5727 299.887 34.33V15.46H305.955Z" stroke="black" style="stroke:black;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_344_209)"/>
<path d="M330.264 53.2L350.022 1.4H357.644L377.328 53.2H370.446L364.97 39.214H342.4L336.924 53.2H330.264ZM344.324 33.368H363.046L353.648 8.43L344.324 33.368Z" stroke="black" style="stroke:black;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_344_209)"/>
<path d="M383.189 1.4H389.627V53.2H383.189V1.4Z" stroke="black" style="stroke:black;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_344_209)"/>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

33
apps/client/src/assets/images/sponsors/logo-testmu-light.svg

@ -0,0 +1,33 @@
<svg width="392" height="61" viewBox="0 0 392 61" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M53.6262 0.138739H37.5382V39.0182H53.6262V0.138739Z" fill="white" style="fill:white;fill-opacity:1;"/>
<path d="M16.0881 0.138729H3.8147e-06V22.2598L16.0881 39.0182V0.138729Z" fill="white" style="fill:white;fill-opacity:1;"/>
<path d="M16.0881 39.0182H3.8147e-06V60.4689H16.0881V39.0182Z" fill="white" style="fill:white;fill-opacity:1;"/>
<path d="M37.539 39.0181H16.0883L28.8247 55.1062H37.539V39.0181Z" fill="white" style="fill:white;fill-opacity:1;"/>
<mask id="path-5-outside-1_342_143" maskUnits="userSpaceOnUse" x="67.6262" y="-0.6" width="324" height="56" fill="black">
<rect fill="white" x="67.6262" y="-0.6" width="324" height="56"/>
<path d="M69.6262 1.4H109.66V7.32H92.8622V53.2H86.4242V7.32H69.6262V1.4Z"/>
<path d="M107.66 34.33C107.66 30.482 108.4 27.1027 109.88 24.192C111.41 21.2813 113.531 19.0367 116.244 17.458C118.958 15.83 122.066 15.016 125.568 15.016C129.022 15.016 132.08 15.7313 134.744 17.162C137.408 18.5927 139.505 20.6647 141.034 23.378C142.564 26.0913 143.378 29.2733 143.476 32.924C143.476 33.4667 143.427 34.33 143.328 35.514H114.024V36.032C114.123 39.732 115.258 42.692 117.428 44.912C119.599 47.132 122.436 48.242 125.938 48.242C128.652 48.242 130.946 47.576 132.82 46.244C134.744 44.8627 136.027 42.9633 136.668 40.546H142.81C142.07 44.394 140.22 47.5513 137.26 50.018C134.3 52.4353 130.674 53.644 126.382 53.644C122.633 53.644 119.352 52.8547 116.54 51.276C113.728 49.648 111.533 47.3787 109.954 44.468C108.425 41.508 107.66 38.1287 107.66 34.33ZM137.186 30.482C136.89 27.2753 135.682 24.784 133.56 23.008C131.488 21.232 128.849 20.344 125.642 20.344C122.781 20.344 120.265 21.2813 118.094 23.156C115.924 25.0307 114.69 27.4727 114.394 30.482H137.186Z"/>
<path d="M154.992 40.768C155.091 43.0373 156.077 44.8873 157.952 46.318C159.876 47.7487 162.343 48.464 165.352 48.464C168.016 48.464 170.162 47.946 171.79 46.91C173.467 45.8247 174.306 44.4187 174.306 42.692C174.306 41.212 173.911 40.0773 173.122 39.288C172.333 38.4987 171.297 37.956 170.014 37.66C168.781 37.364 167.005 37.068 164.686 36.772C161.43 36.3773 158.766 35.8593 156.694 35.218C154.622 34.5767 152.945 33.5653 151.662 32.184C150.379 30.7533 149.738 28.8047 149.738 26.338C149.738 24.118 150.355 22.1447 151.588 20.418C152.871 18.6913 154.597 17.3593 156.768 16.422C158.939 15.4353 161.381 14.942 164.094 14.942C168.534 14.942 172.135 15.978 174.898 18.05C177.71 20.122 179.264 23.082 179.56 26.93H173.492C173.245 24.9567 172.283 23.3287 170.606 22.046C168.929 20.7633 166.832 20.122 164.316 20.122C161.751 20.122 159.679 20.6647 158.1 21.75C156.521 22.786 155.732 24.1673 155.732 25.894C155.732 27.1767 156.102 28.1633 156.842 28.854C157.582 29.5447 158.519 30.0133 159.654 30.26C160.838 30.5067 162.614 30.778 164.982 31.074C168.287 31.4687 171.001 32.0113 173.122 32.702C175.293 33.3927 177.019 34.5027 178.302 36.032C179.634 37.5613 180.3 39.6333 180.3 42.248C180.3 44.5173 179.634 46.5153 178.302 48.242C176.97 49.9687 175.169 51.3007 172.9 52.238C170.631 53.1753 168.115 53.644 165.352 53.644C160.419 53.644 156.447 52.5093 153.438 50.24C150.478 47.9707 148.973 44.8133 148.924 40.768H154.992Z"/>
<path d="M189.7 21.01H182.892V15.46H189.7V4.878H195.768V15.46H205.314V21.01H195.768V43.284C195.768 44.8627 196.064 45.9973 196.656 46.688C197.297 47.3293 198.383 47.65 199.912 47.65H206.498V53.2H199.542C195.99 53.2 193.449 52.4107 191.92 50.832C190.44 49.2533 189.7 46.762 189.7 43.358V21.01Z"/>
<path d="M211.703 1.4H219.843L238.195 44.394L256.325 1.4H264.317V53.2H258.027V12.352L240.859 53.2H235.161L217.993 12.352V53.2H211.703V1.4Z"/>
<path d="M305.955 15.46V53.2H300.701L299.887 48.168C297.026 51.8187 293.005 53.644 287.825 53.644C283.336 53.644 279.734 52.2627 277.021 49.5C274.308 46.7373 272.951 42.396 272.951 36.476V15.46H279.019V36.106C279.019 40.0033 279.858 42.988 281.535 45.06C283.262 47.0827 285.704 48.094 288.861 48.094C292.314 48.094 295.003 46.8607 296.927 44.394C298.9 41.9273 299.887 38.5727 299.887 34.33V15.46H305.955Z"/>
<path d="M330.264 53.2L350.022 1.4H357.644L377.328 53.2H370.446L364.97 39.214H342.4L336.924 53.2H330.264ZM344.324 33.368H363.046L353.648 8.43L344.324 33.368Z"/>
<path d="M383.189 1.4H389.627V53.2H383.189V1.4Z"/>
</mask>
<path d="M69.6262 1.4H109.66V7.32H92.8622V53.2H86.4242V7.32H69.6262V1.4Z" fill="white" style="fill:white;fill-opacity:1;"/>
<path d="M107.66 34.33C107.66 30.482 108.4 27.1027 109.88 24.192C111.41 21.2813 113.531 19.0367 116.244 17.458C118.958 15.83 122.066 15.016 125.568 15.016C129.022 15.016 132.08 15.7313 134.744 17.162C137.408 18.5927 139.505 20.6647 141.034 23.378C142.564 26.0913 143.378 29.2733 143.476 32.924C143.476 33.4667 143.427 34.33 143.328 35.514H114.024V36.032C114.123 39.732 115.258 42.692 117.428 44.912C119.599 47.132 122.436 48.242 125.938 48.242C128.652 48.242 130.946 47.576 132.82 46.244C134.744 44.8627 136.027 42.9633 136.668 40.546H142.81C142.07 44.394 140.22 47.5513 137.26 50.018C134.3 52.4353 130.674 53.644 126.382 53.644C122.633 53.644 119.352 52.8547 116.54 51.276C113.728 49.648 111.533 47.3787 109.954 44.468C108.425 41.508 107.66 38.1287 107.66 34.33ZM137.186 30.482C136.89 27.2753 135.682 24.784 133.56 23.008C131.488 21.232 128.849 20.344 125.642 20.344C122.781 20.344 120.265 21.2813 118.094 23.156C115.924 25.0307 114.69 27.4727 114.394 30.482H137.186Z" fill="white" style="fill:white;fill-opacity:1;"/>
<path d="M154.992 40.768C155.091 43.0373 156.077 44.8873 157.952 46.318C159.876 47.7487 162.343 48.464 165.352 48.464C168.016 48.464 170.162 47.946 171.79 46.91C173.467 45.8247 174.306 44.4187 174.306 42.692C174.306 41.212 173.911 40.0773 173.122 39.288C172.333 38.4987 171.297 37.956 170.014 37.66C168.781 37.364 167.005 37.068 164.686 36.772C161.43 36.3773 158.766 35.8593 156.694 35.218C154.622 34.5767 152.945 33.5653 151.662 32.184C150.379 30.7533 149.738 28.8047 149.738 26.338C149.738 24.118 150.355 22.1447 151.588 20.418C152.871 18.6913 154.597 17.3593 156.768 16.422C158.939 15.4353 161.381 14.942 164.094 14.942C168.534 14.942 172.135 15.978 174.898 18.05C177.71 20.122 179.264 23.082 179.56 26.93H173.492C173.245 24.9567 172.283 23.3287 170.606 22.046C168.929 20.7633 166.832 20.122 164.316 20.122C161.751 20.122 159.679 20.6647 158.1 21.75C156.521 22.786 155.732 24.1673 155.732 25.894C155.732 27.1767 156.102 28.1633 156.842 28.854C157.582 29.5447 158.519 30.0133 159.654 30.26C160.838 30.5067 162.614 30.778 164.982 31.074C168.287 31.4687 171.001 32.0113 173.122 32.702C175.293 33.3927 177.019 34.5027 178.302 36.032C179.634 37.5613 180.3 39.6333 180.3 42.248C180.3 44.5173 179.634 46.5153 178.302 48.242C176.97 49.9687 175.169 51.3007 172.9 52.238C170.631 53.1753 168.115 53.644 165.352 53.644C160.419 53.644 156.447 52.5093 153.438 50.24C150.478 47.9707 148.973 44.8133 148.924 40.768H154.992Z" fill="white" style="fill:white;fill-opacity:1;"/>
<path d="M189.7 21.01H182.892V15.46H189.7V4.878H195.768V15.46H205.314V21.01H195.768V43.284C195.768 44.8627 196.064 45.9973 196.656 46.688C197.297 47.3293 198.383 47.65 199.912 47.65H206.498V53.2H199.542C195.99 53.2 193.449 52.4107 191.92 50.832C190.44 49.2533 189.7 46.762 189.7 43.358V21.01Z" fill="white" style="fill:white;fill-opacity:1;"/>
<path d="M211.703 1.4H219.843L238.195 44.394L256.325 1.4H264.317V53.2H258.027V12.352L240.859 53.2H235.161L217.993 12.352V53.2H211.703V1.4Z" fill="white" style="fill:white;fill-opacity:1;"/>
<path d="M305.955 15.46V53.2H300.701L299.887 48.168C297.026 51.8187 293.005 53.644 287.825 53.644C283.336 53.644 279.734 52.2627 277.021 49.5C274.308 46.7373 272.951 42.396 272.951 36.476V15.46H279.019V36.106C279.019 40.0033 279.858 42.988 281.535 45.06C283.262 47.0827 285.704 48.094 288.861 48.094C292.314 48.094 295.003 46.8607 296.927 44.394C298.9 41.9273 299.887 38.5727 299.887 34.33V15.46H305.955Z" fill="white" style="fill:white;fill-opacity:1;"/>
<path d="M330.264 53.2L350.022 1.4H357.644L377.328 53.2H370.446L364.97 39.214H342.4L336.924 53.2H330.264ZM344.324 33.368H363.046L353.648 8.43L344.324 33.368Z" fill="white" style="fill:white;fill-opacity:1;"/>
<path d="M383.189 1.4H389.627V53.2H383.189V1.4Z" fill="white" style="fill:white;fill-opacity:1;"/>
<path d="M69.6262 1.4H109.66V7.32H92.8622V53.2H86.4242V7.32H69.6262V1.4Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_342_143)"/>
<path d="M107.66 34.33C107.66 30.482 108.4 27.1027 109.88 24.192C111.41 21.2813 113.531 19.0367 116.244 17.458C118.958 15.83 122.066 15.016 125.568 15.016C129.022 15.016 132.08 15.7313 134.744 17.162C137.408 18.5927 139.505 20.6647 141.034 23.378C142.564 26.0913 143.378 29.2733 143.476 32.924C143.476 33.4667 143.427 34.33 143.328 35.514H114.024V36.032C114.123 39.732 115.258 42.692 117.428 44.912C119.599 47.132 122.436 48.242 125.938 48.242C128.652 48.242 130.946 47.576 132.82 46.244C134.744 44.8627 136.027 42.9633 136.668 40.546H142.81C142.07 44.394 140.22 47.5513 137.26 50.018C134.3 52.4353 130.674 53.644 126.382 53.644C122.633 53.644 119.352 52.8547 116.54 51.276C113.728 49.648 111.533 47.3787 109.954 44.468C108.425 41.508 107.66 38.1287 107.66 34.33ZM137.186 30.482C136.89 27.2753 135.682 24.784 133.56 23.008C131.488 21.232 128.849 20.344 125.642 20.344C122.781 20.344 120.265 21.2813 118.094 23.156C115.924 25.0307 114.69 27.4727 114.394 30.482H137.186Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_342_143)"/>
<path d="M154.992 40.768C155.091 43.0373 156.077 44.8873 157.952 46.318C159.876 47.7487 162.343 48.464 165.352 48.464C168.016 48.464 170.162 47.946 171.79 46.91C173.467 45.8247 174.306 44.4187 174.306 42.692C174.306 41.212 173.911 40.0773 173.122 39.288C172.333 38.4987 171.297 37.956 170.014 37.66C168.781 37.364 167.005 37.068 164.686 36.772C161.43 36.3773 158.766 35.8593 156.694 35.218C154.622 34.5767 152.945 33.5653 151.662 32.184C150.379 30.7533 149.738 28.8047 149.738 26.338C149.738 24.118 150.355 22.1447 151.588 20.418C152.871 18.6913 154.597 17.3593 156.768 16.422C158.939 15.4353 161.381 14.942 164.094 14.942C168.534 14.942 172.135 15.978 174.898 18.05C177.71 20.122 179.264 23.082 179.56 26.93H173.492C173.245 24.9567 172.283 23.3287 170.606 22.046C168.929 20.7633 166.832 20.122 164.316 20.122C161.751 20.122 159.679 20.6647 158.1 21.75C156.521 22.786 155.732 24.1673 155.732 25.894C155.732 27.1767 156.102 28.1633 156.842 28.854C157.582 29.5447 158.519 30.0133 159.654 30.26C160.838 30.5067 162.614 30.778 164.982 31.074C168.287 31.4687 171.001 32.0113 173.122 32.702C175.293 33.3927 177.019 34.5027 178.302 36.032C179.634 37.5613 180.3 39.6333 180.3 42.248C180.3 44.5173 179.634 46.5153 178.302 48.242C176.97 49.9687 175.169 51.3007 172.9 52.238C170.631 53.1753 168.115 53.644 165.352 53.644C160.419 53.644 156.447 52.5093 153.438 50.24C150.478 47.9707 148.973 44.8133 148.924 40.768H154.992Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_342_143)"/>
<path d="M189.7 21.01H182.892V15.46H189.7V4.878H195.768V15.46H205.314V21.01H195.768V43.284C195.768 44.8627 196.064 45.9973 196.656 46.688C197.297 47.3293 198.383 47.65 199.912 47.65H206.498V53.2H199.542C195.99 53.2 193.449 52.4107 191.92 50.832C190.44 49.2533 189.7 46.762 189.7 43.358V21.01Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_342_143)"/>
<path d="M211.703 1.4H219.843L238.195 44.394L256.325 1.4H264.317V53.2H258.027V12.352L240.859 53.2H235.161L217.993 12.352V53.2H211.703V1.4Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_342_143)"/>
<path d="M305.955 15.46V53.2H300.701L299.887 48.168C297.026 51.8187 293.005 53.644 287.825 53.644C283.336 53.644 279.734 52.2627 277.021 49.5C274.308 46.7373 272.951 42.396 272.951 36.476V15.46H279.019V36.106C279.019 40.0033 279.858 42.988 281.535 45.06C283.262 47.0827 285.704 48.094 288.861 48.094C292.314 48.094 295.003 46.8607 296.927 44.394C298.9 41.9273 299.887 38.5727 299.887 34.33V15.46H305.955Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_342_143)"/>
<path d="M330.264 53.2L350.022 1.4H357.644L377.328 53.2H370.446L364.97 39.214H342.4L336.924 53.2H330.264ZM344.324 33.368H363.046L353.648 8.43L344.324 33.368Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_342_143)"/>
<path d="M383.189 1.4H389.627V53.2H383.189V1.4Z" stroke="white" style="stroke:white;stroke-opacity:1;" stroke-width="2.8" mask="url(#path-5-outside-1_342_143)"/>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

3
apps/client/src/environments/environment.prod.ts

@ -2,6 +2,5 @@ import type { GfEnvironment } from '@ghostfolio/ui/environment';
export const environment: GfEnvironment = { export const environment: GfEnvironment = {
lastPublish: '{BUILD_TIMESTAMP}', lastPublish: '{BUILD_TIMESTAMP}',
production: true, production: true
stripePublicKey: ''
}; };

3
apps/client/src/environments/environment.ts

@ -6,8 +6,7 @@ import type { GfEnvironment } from '@ghostfolio/ui/environment';
export const environment: GfEnvironment = { export const environment: GfEnvironment = {
lastPublish: null, lastPublish: null,
production: false, production: false
stripePublicKey: ''
}; };
/* /*

406
apps/client/src/locales/messages.ca.xlf

File diff suppressed because it is too large

406
apps/client/src/locales/messages.de.xlf

File diff suppressed because it is too large

578
apps/client/src/locales/messages.es.xlf

File diff suppressed because it is too large

406
apps/client/src/locales/messages.fr.xlf

File diff suppressed because it is too large

406
apps/client/src/locales/messages.it.xlf

File diff suppressed because it is too large

548
apps/client/src/locales/messages.ko.xlf

File diff suppressed because it is too large

406
apps/client/src/locales/messages.nl.xlf

File diff suppressed because it is too large

406
apps/client/src/locales/messages.pl.xlf

File diff suppressed because it is too large

406
apps/client/src/locales/messages.pt.xlf

File diff suppressed because it is too large

406
apps/client/src/locales/messages.tr.xlf

File diff suppressed because it is too large

406
apps/client/src/locales/messages.uk.xlf

File diff suppressed because it is too large

400
apps/client/src/locales/messages.xlf

File diff suppressed because it is too large

432
apps/client/src/locales/messages.zh.xlf

File diff suppressed because it is too large

4
libs/common/eslint.config.cjs

@ -18,7 +18,9 @@ module.exports = [
{ {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],
// Override or add rules here // Override or add rules here
rules: {} rules: {
'@typescript-eslint/prefer-nullish-coalescing': 'error'
}
}, },
{ {
files: ['**/*.js', '**/*.jsx'], files: ['**/*.js', '**/*.jsx'],

4
libs/common/src/lib/calculation-helper.ts

@ -9,7 +9,7 @@ import {
subDays, subDays,
subYears subYears
} from 'date-fns'; } from 'date-fns';
import { isNumber } from 'lodash'; import { isFinite, isNumber } from 'lodash';
import { resetHours } from './helper'; import { resetHours } from './helper';
import { DateRange } from './types'; import { DateRange } from './types';
@ -28,7 +28,7 @@ export function getAnnualizedPerformancePercent({
exponent exponent
); );
if (!isNaN(growthFactor)) { if (isFinite(growthFactor)) {
return new Big(growthFactor).minus(1); return new Big(growthFactor).minus(1);
} }
} }

57
libs/common/src/lib/chart-helper.ts

@ -1,4 +1,13 @@
import { Chart, TooltipPosition } from 'chart.js'; import type { ElementRef } from '@angular/core';
import type {
Chart,
ChartType,
ControllerDatasetOptions,
Plugin,
Point,
TooltipOptions,
TooltipPosition
} from 'chart.js';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { import {
@ -15,7 +24,7 @@ export function formatGroupedDate({
date, date,
groupBy groupBy
}: { }: {
date: Date; date: number;
groupBy: GroupBy; groupBy: GroupBy;
}) { }) {
if (groupBy === 'month') { if (groupBy === 'month') {
@ -27,47 +36,55 @@ export function formatGroupedDate({
return format(date, DATE_FORMAT); return format(date, DATE_FORMAT);
} }
export function getTooltipOptions({ export function getTooltipOptions<T extends ChartType>({
colorScheme, colorScheme,
currency = '', currency = '',
groupBy, groupBy,
locale = getLocale(), locale = getLocale(),
unit = '' unit = ''
}: { }: {
colorScheme?: ColorScheme; colorScheme: ColorScheme;
currency?: string; currency?: string;
groupBy?: GroupBy; groupBy?: GroupBy;
locale?: string; locale?: string;
unit?: string; unit?: string;
} = {}) { }): Partial<TooltipOptions<T>> {
return { return {
backgroundColor: getBackgroundColor(colorScheme), backgroundColor: getBackgroundColor(colorScheme),
bodyColor: `rgb(${getTextColor(colorScheme)})`, bodyColor: `rgb(${getTextColor(colorScheme)})`,
borderWidth: 1, borderWidth: 1,
borderColor: `rgba(${getTextColor(colorScheme)}, 0.1)`, borderColor: `rgba(${getTextColor(colorScheme)}, 0.1)`,
// @ts-expect-error: no need to set all attributes in callbacks
callbacks: { callbacks: {
label: (context) => { label: (context) => {
let label = context.dataset.label || ''; let label = (context.dataset as ControllerDatasetOptions).label ?? '';
if (label) { if (label) {
label += ': '; label += ': ';
} }
if (context.parsed.y !== null) {
const yPoint = (context.parsed as Point).y;
if (yPoint !== null) {
if (currency) { if (currency) {
label += `${context.parsed.y.toLocaleString(locale, { label += `${yPoint.toLocaleString(locale, {
maximumFractionDigits: 2, maximumFractionDigits: 2,
minimumFractionDigits: 2 minimumFractionDigits: 2
})} ${currency}`; })} ${currency}`;
} else if (unit) { } else if (unit) {
label += `${context.parsed.y.toFixed(2)} ${unit}`; label += `${yPoint.toFixed(2)} ${unit}`;
} else { } else {
label += context.parsed.y.toFixed(2); label += yPoint.toFixed(2);
} }
} }
return label; return label;
}, },
title: (contexts) => { title: (contexts) => {
if (groupBy) { const xPoint = (contexts[0].parsed as Point).x;
return formatGroupedDate({ groupBy, date: contexts[0].parsed.x });
if (groupBy && xPoint !== null) {
return formatGroupedDate({ groupBy, date: xPoint });
} }
return contexts[0].label; return contexts[0].label;
@ -92,16 +109,17 @@ export function getTooltipPositionerMapTop(
if (!position || !chart?.chartArea) { if (!position || !chart?.chartArea) {
return false; return false;
} }
return { return {
x: position.x, x: position.x,
y: chart.chartArea.top y: chart.chartArea.top
}; };
} }
export function getVerticalHoverLinePlugin( export function getVerticalHoverLinePlugin<T extends 'line' | 'bar'>(
chartCanvas, chartCanvas: ElementRef<HTMLCanvasElement>,
colorScheme?: ColorScheme colorScheme: ColorScheme
) { ): Plugin<T, { color: string; width: number }> {
return { return {
afterDatasetsDraw: (chart, _, options) => { afterDatasetsDraw: (chart, _, options) => {
const active = chart.getActiveElements(); const active = chart.getActiveElements();
@ -110,8 +128,8 @@ export function getVerticalHoverLinePlugin(
return; return;
} }
const color = options.color || `rgb(${getTextColor(colorScheme)})`; const color = options.color ?? `rgb(${getTextColor(colorScheme)})`;
const width = options.width || 1; const width = options.width ?? 1;
const { const {
chartArea: { bottom, top } chartArea: { bottom, top }
@ -119,6 +137,8 @@ export function getVerticalHoverLinePlugin(
const xValue = active[0].element.x; const xValue = active[0].element.x;
const context = chartCanvas.nativeElement.getContext('2d'); const context = chartCanvas.nativeElement.getContext('2d');
if (context) {
context.lineWidth = width; context.lineWidth = width;
context.strokeStyle = color; context.strokeStyle = color;
@ -126,6 +146,7 @@ export function getVerticalHoverLinePlugin(
context.moveTo(xValue, top); context.moveTo(xValue, top);
context.lineTo(xValue, bottom); context.lineTo(xValue, bottom);
context.stroke(); context.stroke();
}
}, },
id: 'verticalHoverLine' id: 'verticalHoverLine'
}; };

2
libs/common/src/lib/class-transformer.ts

@ -16,7 +16,7 @@ export function transformToMapOfBig({
return mapOfBig; return mapOfBig;
} }
export function transformToBig({ value }: { value: string }): Big { export function transformToBig({ value }: { value: string }): Big | null {
if (value === null) { if (value === null) {
return null; return null;
} }

52
libs/common/src/lib/config.ts

@ -78,6 +78,58 @@ export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY = 1;
export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1;
export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000;
export const DEFAULT_REDACTED_PATHS = [
'accounts[*].balance',
'accounts[*].valueInBaseCurrency',
'activities[*].account.balance',
'activities[*].account.comment',
'activities[*].comment',
'activities[*].fee',
'activities[*].feeInAssetProfileCurrency',
'activities[*].feeInBaseCurrency',
'activities[*].quantity',
'activities[*].SymbolProfile.symbolMapping',
'activities[*].SymbolProfile.watchedByCount',
'activities[*].value',
'activities[*].valueInBaseCurrency',
'balance',
'balanceInBaseCurrency',
'balances[*].account.balance',
'balances[*].account.comment',
'balances[*].value',
'balances[*].valueInBaseCurrency',
'comment',
'dividendInBaseCurrency',
'feeInBaseCurrency',
'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'historicalData[*].quantity',
'holdings[*].dividend',
'holdings[*].grossPerformance',
'holdings[*].grossPerformanceWithCurrencyEffect',
'holdings[*].holdings[*].valueInBaseCurrency',
'holdings[*].investment',
'holdings[*].netPerformance',
'holdings[*].netPerformanceWithCurrencyEffect',
'holdings[*].quantity',
'holdings[*].valueInBaseCurrency',
'interestInBaseCurrency',
'investmentInBaseCurrencyWithCurrencyEffect',
'netPerformance',
'netPerformanceWithCurrencyEffect',
'platforms[*].balance',
'platforms[*].valueInBaseCurrency',
'quantity',
'SymbolProfile.symbolMapping',
'SymbolProfile.watchedByCount',
'totalBalanceInBaseCurrency',
'totalDividendInBaseCurrency',
'totalInterestInBaseCurrency',
'totalValueInBaseCurrency',
'value',
'valueInBaseCurrency'
];
// USX is handled separately // USX is handled separately
export const DERIVED_CURRENCIES = [ export const DERIVED_CURRENCIES = [
{ {

22
libs/common/src/lib/helper.ts

@ -144,7 +144,7 @@ export function extractNumberFromString({
}: { }: {
locale?: string; locale?: string;
value: string; value: string;
}): number { }): number | undefined {
try { try {
// Remove non-numeric characters (excluding international formatting characters) // Remove non-numeric characters (excluding international formatting characters)
const numericValue = value.replace(/[^\d.,'’\s]/g, ''); const numericValue = value.replace(/[^\d.,'’\s]/g, '');
@ -223,8 +223,8 @@ export function getDateFormatString(aLocale?: string) {
); );
return formatObject return formatObject
.map((object) => { .map(({ type, value }) => {
switch (object.type) { switch (type) {
case 'day': case 'day':
return 'dd'; return 'dd';
case 'month': case 'month':
@ -232,7 +232,7 @@ export function getDateFormatString(aLocale?: string) {
case 'year': case 'year':
return 'yyyy'; return 'yyyy';
default: default:
return object.value; return value;
} }
}) })
.join(''); .join('');
@ -271,9 +271,9 @@ export function getLowercase(object: object, path: string) {
export function getNumberFormatDecimal(aLocale?: string) { export function getNumberFormatDecimal(aLocale?: string) {
const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99); const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99);
return formatObject.find((object) => { return formatObject.find(({ type }) => {
return object.type === 'decimal'; return type === 'decimal';
}).value; })?.value;
} }
export function getNumberFormatGroup(aLocale = getLocale()) { export function getNumberFormatGroup(aLocale = getLocale()) {
@ -281,9 +281,9 @@ export function getNumberFormatGroup(aLocale = getLocale()) {
useGrouping: true useGrouping: true
}).formatToParts(9999.99); }).formatToParts(9999.99);
return formatObject.find((object) => { return formatObject.find(({ type }) => {
return object.type === 'group'; return type === 'group';
}).value; })?.value;
} }
export function getStartOfUtcDate(aDate: Date) { export function getStartOfUtcDate(aDate: Date) {
@ -394,7 +394,7 @@ export function isRootCurrency(aCurrency: string) {
}); });
} }
export function parseDate(date: string): Date { export function parseDate(date: string): Date | undefined {
if (!date) { if (!date) {
return undefined; return undefined;
} }

2
libs/common/src/lib/interfaces/activities.interface.ts

@ -8,7 +8,7 @@ export interface Activity extends Order {
error?: ActivityError; error?: ActivityError;
feeInAssetProfileCurrency: number; feeInAssetProfileCurrency: number;
feeInBaseCurrency: number; feeInBaseCurrency: number;
SymbolProfile?: EnhancedSymbolProfile; SymbolProfile: EnhancedSymbolProfile;
tagIds?: string[]; tagIds?: string[];
tags?: Tag[]; tags?: Tag[];
unitPriceInAssetProfileCurrency: number; unitPriceInAssetProfileCurrency: number;

2
libs/common/src/lib/interfaces/admin-data.interface.ts

@ -1,12 +1,12 @@
import { DataProviderInfo } from './data-provider-info.interface'; import { DataProviderInfo } from './data-provider-info.interface';
export interface AdminData { export interface AdminData {
activitiesCount: number;
dataProviders: (DataProviderInfo & { dataProviders: (DataProviderInfo & {
assetProfileCount: number; assetProfileCount: number;
useForExchangeRates: boolean; useForExchangeRates: boolean;
})[]; })[];
settings: { [key: string]: boolean | object | string | string[] }; settings: { [key: string]: boolean | object | string | string[] };
transactionCount: number;
userCount: number; userCount: number;
version: string; version: string;
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save