Browse Source

Merge remote-tracking branch 'origin/next' into task/upgrade-to-prisma-7

pull/6027/head
KenTandrian 2 weeks ago
parent
commit
fedcc0c816
  1. 4
      .vscode/extensions.json
  2. 2
      .vscode/settings.json
  3. 62
      CHANGELOG.md
  4. 50
      README.md
  5. 4
      apps/api/src/app/endpoints/platforms/platforms.controller.ts
  6. 6
      apps/api/src/app/import/import.service.ts
  7. 7
      apps/api/src/app/info/info.service.ts
  8. 49
      apps/api/src/app/order/order.service.ts
  9. 58
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  10. 13
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts
  11. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
  12. 13
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts
  13. 14
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts
  14. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts
  15. 13
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  16. 16
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  17. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts
  18. 13
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  19. 150
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts
  20. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
  21. 12
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts
  22. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts
  23. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts
  24. 4
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
  25. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts
  26. 12
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  27. 12
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
  28. 5
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts
  29. 32
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  30. 1
      apps/api/src/app/portfolio/interfaces/portfolio-order-item.interface.ts
  31. 1
      apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts
  32. 8
      apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts
  33. 33
      apps/api/src/app/portfolio/portfolio.service.ts
  34. 210
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  35. 6
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts
  36. 3
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts
  37. 15
      apps/client/project.json
  38. 2
      apps/client/src/app/app.component.ts
  39. 15
      apps/client/src/app/components/footer/footer.component.html
  40. 60
      apps/client/src/app/components/header/header.component.html
  41. 2
      apps/client/src/app/components/header/header.component.scss
  42. 14
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  43. 4
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  44. 1
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  45. 13
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  46. 18
      apps/client/src/app/pages/about/overview/about-overview-page.html
  47. 5
      apps/client/src/app/pages/features/features-page.html
  48. 11
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  49. 66
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  50. 1
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  51. 5
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
  52. BIN
      apps/client/src/assets/images/sponsors/logo-lambdatest.png
  53. 33
      apps/client/src/assets/images/sponsors/logo-testmu-dark.svg
  54. 33
      apps/client/src/assets/images/sponsors/logo-testmu-light.svg
  55. 3
      apps/client/src/environments/environment.prod.ts
  56. 3
      apps/client/src/environments/environment.ts
  57. 330
      apps/client/src/locales/messages.ca.xlf
  58. 330
      apps/client/src/locales/messages.de.xlf
  59. 330
      apps/client/src/locales/messages.es.xlf
  60. 330
      apps/client/src/locales/messages.fr.xlf
  61. 330
      apps/client/src/locales/messages.it.xlf
  62. 8737
      apps/client/src/locales/messages.ko.xlf
  63. 330
      apps/client/src/locales/messages.nl.xlf
  64. 330
      apps/client/src/locales/messages.pl.xlf
  65. 330
      apps/client/src/locales/messages.pt.xlf
  66. 330
      apps/client/src/locales/messages.tr.xlf
  67. 330
      apps/client/src/locales/messages.uk.xlf
  68. 327
      apps/client/src/locales/messages.xlf
  69. 330
      apps/client/src/locales/messages.zh.xlf
  70. 83
      apps/client/src/styles.scss
  71. 155
      apps/client/src/styles/m3-theme.scss
  72. 506
      apps/client/src/styles/theme.scss
  73. 1
      libs/common/src/lib/config.ts
  74. 28
      libs/common/src/lib/helper.ts
  75. 2
      libs/common/src/lib/interfaces/index.ts
  76. 6
      libs/common/src/lib/interfaces/info-item.interface.ts
  77. 4
      libs/common/src/lib/interfaces/portfolio-position.interface.ts
  78. 27
      libs/common/src/lib/interfaces/position.interface.ts
  79. 3
      libs/common/src/lib/interfaces/responses/accounts-response.interface.ts
  80. 6
      libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
  81. 7
      libs/common/src/lib/models/timeline-position.ts
  82. 4
      libs/common/src/lib/types/account-with-value.type.ts
  83. 6
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  84. 11
      libs/ui/src/lib/benchmark/benchmark.component.ts
  85. 1
      libs/ui/src/lib/environment/environment.interface.ts
  86. 7
      libs/ui/src/lib/holdings-table/holdings-table.component.html
  87. 4
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  88. 7
      libs/ui/src/lib/mocks/holdings.ts
  89. 19
      libs/ui/src/lib/services/data.service.ts
  90. 20
      package-lock.json
  91. 6
      package.json

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
} }

62
CHANGELOG.md

@ -5,7 +5,63 @@ 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 ## Next
### Changed
- Migrated from _Material Design_ 2 to _Material Design_ 3
## 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
- 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
### Changed ### Changed
@ -19,7 +75,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed the net worth calculation to prevent the double counting of cash positions
- Fixed the filtering by asset class in the endpoint `GET api/v1/portfolio/holdings` - Fixed the filtering by asset class in the endpoint `GET api/v1/portfolio/holdings`
- Fixed the case-insensitive sorting in the accounts table component
- Fixed the case-insensitive sorting in the benchmark component
- Fixed the case-insensitive sorting in the holdings table component
## 2.228.0 - 2026-01-03 ## 2.228.0 - 2026-01-03

50
README.md

@ -85,31 +85,32 @@ 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` |
| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on | | `ENABLE_FEATURE_AUTH_TOKEN` | `boolean` (optional) | `true` | Enables authentication via security token |
| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) | | `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on |
| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` | | `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) |
| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on | | `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` |
| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database | | `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on |
| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database | | `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database |
| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database | | `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database |
| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ | | `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database |
| `REDIS_HOST` | `string` | | The host where _Redis_ is running | | `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ |
| `REDIS_PASSWORD` | `string` | | The password of _Redis_ | | `REDIS_HOST` | `string` | | The host where _Redis_ is running |
| `REDIS_PORT` | `number` | | The port where _Redis_ is running | | `REDIS_PASSWORD` | `string` | | The password of _Redis_ |
| `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds | | `REDIS_PORT` | `number` | | The port where _Redis_ is running |
| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. | | `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds |
| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. |
#### OpenID Connect OIDC (Experimental) #### OpenID Connect OIDC (Experimental)
| 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.testmu.ai?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.testmu.ai/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"> </a>
<img alt="LambdaTest Logo" height="45" width="250" src="https://www.lambdatest.com/blue-logo.png" />
</a>
</p>
</div> </div>
## Analytics ## Analytics

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,

49
apps/api/src/app/order/order.service.ts

@ -743,47 +743,50 @@ export class OrderService {
/** /**
* Retrieves all orders required for the portfolio calculator, including both standard asset orders * Retrieves all orders required for the portfolio calculator, including both standard asset orders
* and synthetic orders representing cash activities. * and optional synthetic orders representing cash activities.
*
* @param filters - Optional filters to apply to the orders.
* @param userCurrency - The base currency of the user.
* @param userId - The ID of the user.
* @returns An object containing the combined list of activities and the total count.
*/ */
@LogPerformance @LogPerformance
public async getOrdersForPortfolioCalculator({ public async getOrdersForPortfolioCalculator({
filters, filters,
userCurrency, userCurrency,
userId userId,
withCash = false
}: { }: {
/** Optional filters to apply to the orders. */
filters?: Filter[]; filters?: Filter[];
/** The base currency of the user. */
userCurrency: string; userCurrency: string;
/** The ID of the user. */
userId: string; userId: string;
/** Whether to include cash activities in the result. */
withCash?: boolean;
}) { }) {
const nonCashOrders = await this.getOrders({ const orders = await this.getOrders({
filters, filters,
userCurrency, userCurrency,
userId, userId,
withExcludedAccountsAndActivities: false // TODO withExcludedAccountsAndActivities: false // TODO
}); });
const cashDetails = await this.accountService.getCashDetails({ if (withCash) {
filters, const cashDetails = await this.accountService.getCashDetails({
userId, filters,
currency: userCurrency userId,
}); currency: userCurrency
});
const cashOrders = await this.getCashOrders({ const cashOrders = await this.getCashOrders({
cashDetails, cashDetails,
filters, filters,
userCurrency, userCurrency,
userId userId
}); });
return { orders.activities.push(...cashOrders.activities);
activities: [...nonCashOrders.activities, ...cashOrders.activities], orders.count += cashOrders.count;
count: nonCashOrders.count + cashOrders.count }
};
return orders;
} }
public async getStatisticsByCurrency( public async getStatisticsByCurrency(

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

@ -39,6 +39,7 @@ import { GroupBy } from '@ghostfolio/common/types';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { AssetSubClass } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { plainToClass } from 'class-transformer'; import { plainToClass } from 'class-transformer';
import { import {
@ -119,6 +120,7 @@ export abstract class PortfolioCalculator {
({ ({
date, date,
feeInAssetProfileCurrency, feeInAssetProfileCurrency,
feeInBaseCurrency,
quantity, quantity,
SymbolProfile, SymbolProfile,
tags = [], tags = [],
@ -141,6 +143,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)
}; };
@ -335,12 +338,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(
@ -389,28 +386,36 @@ export abstract class PortfolioCalculator {
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
valuesBySymbol[item.symbol] = { const includeInTotalAssetValue =
currentValues, item.assetSubClass !== AssetSubClass.CASH;
currentValuesWithCurrencyEffect,
investmentValuesAccumulated, if (includeInTotalAssetValue) {
investmentValuesAccumulatedWithCurrencyEffect, valuesBySymbol[item.symbol] = {
investmentValuesWithCurrencyEffect, currentValues,
netPerformanceValues, currentValuesWithCurrencyEffect,
netPerformanceValuesWithCurrencyEffect, investmentValuesAccumulated,
timeWeightedInvestmentValues, investmentValuesAccumulatedWithCurrencyEffect,
timeWeightedInvestmentValuesWithCurrencyEffect investmentValuesWithCurrencyEffect,
}; netPerformanceValues,
netPerformanceValuesWithCurrencyEffect,
timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect
};
}
positions.push({ positions.push({
feeInBaseCurrency, includeInTotalAssetValue,
timeWeightedInvestment, timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect, timeWeightedInvestmentWithCurrencyEffect,
dividend: totalDividend, activitiesCount: item.activitiesCount,
dividendInBaseCurrency: totalDividendInBaseCurrency,
averagePrice: item.averagePrice, averagePrice: item.averagePrice,
currency: item.currency, currency: item.currency,
dataSource: item.dataSource, dataSource: item.dataSource,
dateOfFirstActivity: item.dateOfFirstActivity,
dividend: totalDividend,
dividendInBaseCurrency: totalDividendInBaseCurrency,
fee: item.fee, fee: item.fee,
feeInBaseCurrency: item.feeInBaseCurrency,
firstBuyDate: item.firstBuyDate, firstBuyDate: item.firstBuyDate,
grossPerformance: !hasErrors ? (grossPerformance ?? null) : null, grossPerformance: !hasErrors ? (grossPerformance ?? null) : null,
grossPerformancePercentage: !hasErrors grossPerformancePercentage: !hasErrors
@ -426,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)
@ -931,6 +935,7 @@ export abstract class PortfolioCalculator {
for (const { for (const {
date, date,
fee, fee,
feeInBaseCurrency,
quantity, quantity,
SymbolProfile, SymbolProfile,
tags, tags,
@ -990,11 +995,15 @@ 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),
feeInBaseCurrency:
oldAccumulatedSymbol.feeInBaseCurrency.plus(feeInBaseCurrency),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate, firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
includeInHoldings: oldAccumulatedSymbol.includeInHoldings, includeInHoldings: oldAccumulatedSymbol.includeInHoldings,
quantity: newQuantity, quantity: newQuantity,
@ -1007,10 +1016,13 @@ 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, firstBuyDate: date,
includeInHoldings: INVESTMENT_ACTIVITY_TYPES.includes(type), includeInHoldings: INVESTMENT_ACTIVITY_TYPES.includes(type),

13
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,15 +133,22 @@ 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'),
@ -200,6 +209,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 }
]);
}); });
}); });
}); });

14
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,15 +149,22 @@ 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'),
@ -213,6 +223,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 }
]);
}); });
}); });
}); });

13
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,15 +133,22 @@ 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'),
@ -198,6 +207,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 }
]);
}); });
}); });
}); });

14
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,15 +123,22 @@ 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'),
@ -198,6 +206,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 +220,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 +260,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',

13
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,9 +195,11 @@ 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'),
@ -244,6 +252,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 }
]);
}); });
}); });
}); });

16
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,9 +158,11 @@ 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'),
@ -251,6 +260,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',

13
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,9 +195,11 @@ 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'),
@ -244,6 +252,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 }
]);
}); });
}); });
}); });

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

@ -14,7 +14,7 @@ import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-r
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.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 { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { TimelinePosition } from '@ghostfolio/common/models';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
@ -191,7 +191,8 @@ describe('PortfolioCalculator', () => {
const { activities } = await orderService.getOrdersForPortfolioCalculator( const { activities } = await orderService.getOrdersForPortfolioCalculator(
{ {
userCurrency: 'CHF', userCurrency: 'CHF',
userId: userDummyData.id userId: userDummyData.id,
withCash: true
} }
); );
@ -201,7 +202,14 @@ describe('PortfolioCalculator', () => {
values: [] values: []
}); });
const accountBalanceItems =
await accountBalanceService.getAccountBalanceItems({
userCurrency: 'CHF',
userId: userDummyData.id
});
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
accountBalanceItems,
activities, activities,
calculationType: PerformanceCalculationType.ROAI, calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF', currency: 'CHF',
@ -210,94 +218,74 @@ describe('PortfolioCalculator', () => {
const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const historicalData20231231 = portfolioSnapshot.historicalData.find( const position = portfolioSnapshot.positions.find(({ symbol }) => {
({ date }) => { return symbol === 'USD';
return date === '2023-12-31';
}
);
const historicalData20240101 = portfolioSnapshot.historicalData.find(
({ date }) => {
return date === '2024-01-01';
}
);
const historicalData20241231 = portfolioSnapshot.historicalData.find(
({ date }) => {
return date === '2024-12-31';
}
);
/**
* Investment value with currency effect: 1000 USD * 0.85 = 850 CHF
* Total investment: 1000 USD * 0.91 = 910 CHF
* Value (current): 1000 USD * 0.91 = 910 CHF
* Value with currency effect: 1000 USD * 0.85 = 850 CHF
*/
expect(historicalData20231231).toMatchObject({
date: '2023-12-31',
investmentValueWithCurrencyEffect: 850,
netPerformance: 0,
netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0,
netWorth: 850,
totalAccountBalance: 0,
totalInvestment: 910,
totalInvestmentValueWithCurrencyEffect: 850,
value: 910,
valueWithCurrencyEffect: 850
});
/**
* Net performance with currency effect: (1000 * 0.86) - (1000 * 0.85) = 10 CHF
* Total investment: 1000 USD * 0.91 = 910 CHF
* Total investment value with currency effect: 1000 USD * 0.85 = 850 CHF
* Value (current): 1000 USD * 0.91 = 910 CHF
* Value with currency effect: 1000 USD * 0.86 = 860 CHF
*/
expect(historicalData20240101).toMatchObject({
date: '2024-01-01',
investmentValueWithCurrencyEffect: 0,
netPerformance: 0,
netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0.011764705882352941,
netPerformanceWithCurrencyEffect: 10,
netWorth: 860,
totalAccountBalance: 0,
totalInvestment: 910,
totalInvestmentValueWithCurrencyEffect: 850,
value: 910,
valueWithCurrencyEffect: 860
}); });
/** /**
* Investment value with currency effect: 1000 USD * 0.90 = 900 CHF * Investment: 2000 USD * 0.91 = 1820 CHF
* Investment value with currency effect: (1000 USD * 0.85) + (1000 USD * 0.90) = 1750 CHF
* Net performance: (1000 USD * 1.0) - (1000 USD * 1.0) = 0 CHF * Net performance: (1000 USD * 1.0) - (1000 USD * 1.0) = 0 CHF
* Net performance with currency effect: (1000 USD * 0.9) - (1000 USD * 0.85) = 50 CHF * Total account balance: 2000 USD * 0.85 = 1700 CHF (using the exchange rate on 2024-12-31)
* Total investment: 2000 USD * 0.91 = 1820 CHF * Value in base currency: 2000 USD * 0.91 = 1820 CHF
* Total investment value with currency effect: (1000 USD * 0.85) + (1000 USD * 0.90) = 1750 CHF
* Value (current): 2000 USD * 0.91 = 1820 CHF
* Value with currency effect: 2000 USD * 0.9 = 1800 CHF
*/ */
expect(historicalData20241231).toMatchObject<HistoricalDataItem>({ expect(position).toMatchObject<TimelinePosition>({
date: '2024-12-31', activitiesCount: 2,
investmentValueWithCurrencyEffect: 900, averagePrice: new Big(1),
netPerformance: 0, currency: 'USD',
netPerformanceInPercentage: 0, dataSource: DataSource.YAHOO,
netPerformanceInPercentageWithCurrencyEffect: 0.058823529411764705, dateOfFirstActivity: '2023-12-31',
netPerformanceWithCurrencyEffect: 50, dividend: new Big(0),
netWorth: 1800, dividendInBaseCurrency: new Big(0),
totalAccountBalance: 0, fee: new Big(0),
totalInvestment: 1820, feeInBaseCurrency: new Big(0),
totalInvestmentValueWithCurrencyEffect: 1750, firstBuyDate: '2023-12-31',
value: 1820, grossPerformance: new Big(0),
valueWithCurrencyEffect: 1800 grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(
'0.08211603004634809014'
),
grossPerformanceWithCurrencyEffect: new Big(70),
includeInTotalAssetValue: false,
investment: new Big(1820),
investmentWithCurrencyEffect: new Big(1750),
marketPrice: 1,
marketPriceInBaseCurrency: 0.91,
netPerformance: new Big(0),
netPerformancePercentage: new Big(0),
netPerformancePercentageWithCurrencyEffectMap: {
'1d': new Big('0.01111111111111111111'),
'1y': new Big('0.06937181021989792704'),
'5y': new Big('0.0818817546090273363'),
max: new Big('0.0818817546090273363'),
mtd: new Big('0.01111111111111111111'),
wtd: new Big('-0.05517241379310344828'),
ytd: new Big('0.01111111111111111111')
},
netPerformanceWithCurrencyEffectMap: {
'1d': new Big(20),
'1y': new Big(60),
'5y': new Big(70),
max: new Big(70),
mtd: new Big(20),
wtd: new Big(-80),
ytd: new Big(20)
},
quantity: new Big(2000),
symbol: 'USD',
timeWeightedInvestment: new Big('912.47956403269754768392'),
timeWeightedInvestmentWithCurrencyEffect: new Big(
'852.45231607629427792916'
),
transactionCount: 2,
valueInBaseCurrency: new Big(1820)
}); });
expect(portfolioSnapshot).toMatchObject({ expect(portfolioSnapshot).toMatchObject({
hasErrors: false, hasErrors: false,
totalFeesWithCurrencyEffect: new Big('0'), totalFeesWithCurrencyEffect: new Big(0),
totalInterestWithCurrencyEffect: new Big('0'), totalInterestWithCurrencyEffect: new Big(0),
totalLiabilitiesWithCurrencyEffect: new Big('0') totalLiabilitiesWithCurrencyEffect: new Big(0)
}); });
}); });
}); });

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,

12
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,15 +129,22 @@ 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'),
@ -217,6 +225,10 @@ describe('PortfolioCalculator', () => {
investment: 0 investment: 0
} }
]); ]);
expect(investmentsByYear).toEqual([
{ date: '2023-01-01', investment: 82.329056 }
]);
}); });
}); });
}); });

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',

4
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,9 +131,11 @@ 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'),

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([]);
}); });
}); });
}); });

12
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,15 +129,22 @@ 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'),
@ -197,6 +205,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 }
]);
}); });
}); });
}); });

12
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,9 +193,11 @@ 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'),
@ -248,6 +256,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 }
]);
}); });
}); });
}); });

5
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,9 +116,11 @@ 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'),
@ -129,7 +132,7 @@ describe('PortfolioCalculator', () => {
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'),

32
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 {
@ -34,7 +41,11 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let totalTimeWeightedInvestment = new Big(0); let totalTimeWeightedInvestment = new Big(0);
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
for (const currentPosition of positions) { for (const currentPosition of positions.filter(
({ includeInTotalAssetValue }) => {
return includeInTotalAssetValue;
}
)) {
if (currentPosition.feeInBaseCurrency) { if (currentPosition.feeInBaseCurrency) {
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
currentPosition.feeInBaseCurrency currentPosition.feeInBaseCurrency
@ -833,15 +844,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;

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'],

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

@ -2,18 +2,26 @@ 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;
feeInBaseCurrency: Big;
/** @deprecated use dateOfFirstActivity instead */
firstBuyDate: string; firstBuyDate: string;
includeInHoldings: boolean; includeInHoldings: boolean;
investment: Big; investment: Big;
quantity: Big; quantity: Big;
skipErrors: boolean; skipErrors: boolean;
symbol: string; symbol: string;
tags?: Tag[]; tags?: Tag[];
/** @deprecated use activitiesCount instead */
transactionCount: number; transactionCount: number;
} }

33
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(
@ -233,6 +233,7 @@ export class PortfolioService {
account.currency, account.currency,
userCurrency userCurrency
), ),
transactionCount: activitiesCount,
value: this.exchangeRateDataService.toCurrency( value: this.exchangeRateDataService.toCurrency(
valueInBaseCurrency, valueInBaseCurrency,
userCurrency, userCurrency,
@ -262,6 +263,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;
@ -284,6 +287,8 @@ export class PortfolioService {
let transactionCount = 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,6 +301,7 @@ export class PortfolioService {
totalValueInBaseCurrency = totalValueInBaseCurrency.plus( totalValueInBaseCurrency = totalValueInBaseCurrency.plus(
account.valueInBaseCurrency account.valueInBaseCurrency
); );
transactionCount += account.transactionCount; transactionCount += account.transactionCount;
} }
@ -310,6 +316,7 @@ export class PortfolioService {
return { return {
accounts, accounts,
activitiesCount,
transactionCount, transactionCount,
totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(), totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(),
totalDividendInBaseCurrency: totalDividendInBaseCurrency.toNumber(), totalDividendInBaseCurrency: totalDividendInBaseCurrency.toNumber(),
@ -567,6 +574,7 @@ export class PortfolioService {
} }
for (const { for (const {
activitiesCount,
currency, currency,
dividend, dividend,
firstBuyDate, firstBuyDate,
@ -610,6 +618,7 @@ export class PortfolioService {
} }
holdings[symbol] = { holdings[symbol] = {
activitiesCount,
currency, currency,
markets, markets,
marketsAdvanced, marketsAdvanced,
@ -789,10 +798,11 @@ export class PortfolioService {
} }
const { const {
activitiesCount,
averagePrice, averagePrice,
currency, currency,
dividendInBaseCurrency, dividendInBaseCurrency,
fee, feeInBaseCurrency,
firstBuyDate, firstBuyDate,
grossPerformance, grossPerformance,
grossPerformancePercentage, grossPerformancePercentage,
@ -807,8 +817,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 }) => {
@ -914,25 +923,20 @@ export class PortfolioService {
); );
return { return {
firstBuyDate, activitiesCount,
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],
dateOfFirstActivity: firstBuyDate,
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 +1666,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,

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

File diff suppressed because it is too large

6
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts

@ -1,5 +1,7 @@
export const ExchangeRateDataServiceMock = { import { ExchangeRateDataService } from './exchange-rate-data.service';
getExchangeRatesByCurrency: ({ targetCurrency }): Promise<any> => {
export const ExchangeRateDataServiceMock: Partial<ExchangeRateDataService> = {
getExchangeRatesByCurrency: ({ targetCurrency }) => {
if (targetCurrency === 'CHF') { if (targetCurrency === 'CHF') {
return Promise.resolve({ return Promise.resolve({
CHFCHF: { CHFCHF: {

3
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts

@ -50,7 +50,8 @@ export class PortfolioSnapshotProcessor {
await this.orderService.getOrdersForPortfolioCalculator({ await this.orderService.getOrdersForPortfolioCalculator({
filters: job.data.filters, filters: job.data.filters,
userCurrency: job.data.userCurrency, userCurrency: job.data.userCurrency,
userId: job.data.userId userId: job.data.userId,
withCash: true
}); });
const accountBalanceItems = const accountBalanceItems =

15
apps/client/project.json

@ -26,6 +26,10 @@
"baseHref": "/it/", "baseHref": "/it/",
"translation": "apps/client/src/locales/messages.it.xlf" "translation": "apps/client/src/locales/messages.it.xlf"
}, },
"ko": {
"baseHref": "/ko/",
"translation": "apps/client/src/locales/messages.ko.xlf"
},
"nl": { "nl": {
"baseHref": "/nl/", "baseHref": "/nl/",
"translation": "apps/client/src/locales/messages.nl.xlf" "translation": "apps/client/src/locales/messages.nl.xlf"
@ -110,6 +114,10 @@
"baseHref": "/it/", "baseHref": "/it/",
"localize": ["it"] "localize": ["it"]
}, },
"development-ko": {
"baseHref": "/ko/",
"localize": ["ko"]
},
"development-nl": { "development-nl": {
"baseHref": "/nl/", "baseHref": "/nl/",
"localize": ["nl"] "localize": ["nl"]
@ -213,6 +221,9 @@
"sslKey": "apps/client/localhost.pem" "sslKey": "apps/client/localhost.pem"
}, },
"configurations": { "configurations": {
"development-ca": {
"buildTarget": "client:build:development-ca"
},
"development-de": { "development-de": {
"buildTarget": "client:build:development-de" "buildTarget": "client:build:development-de"
}, },
@ -228,6 +239,9 @@
"development-it": { "development-it": {
"buildTarget": "client:build:development-it" "buildTarget": "client:build:development-it"
}, },
"development-ko": {
"buildTarget": "client:build:development-ko"
},
"development-nl": { "development-nl": {
"buildTarget": "client:build:development-nl" "buildTarget": "client:build:development-nl"
}, },
@ -264,6 +278,7 @@
"messages.es.xlf", "messages.es.xlf",
"messages.fr.xlf", "messages.fr.xlf",
"messages.it.xlf", "messages.it.xlf",
"messages.ko.xlf",
"messages.nl.xlf", "messages.nl.xlf",
"messages.pl.xlf", "messages.pl.xlf",
"messages.pt.xlf", "messages.pt.xlf",

2
apps/client/src/app/app.component.ts

@ -336,7 +336,9 @@ export class GfAppComponent implements OnDestroy, OnInit {
if (isDarkTheme) { if (isDarkTheme) {
this.document.body.classList.add('theme-dark'); this.document.body.classList.add('theme-dark');
this.document.body.classList.remove('theme-light');
} else { } else {
this.document.body.classList.add('theme-light');
this.document.body.classList.remove('theme-dark'); this.document.body.classList.remove('theme-dark');
} }

15
apps/client/src/app/components/footer/footer.component.html

@ -122,7 +122,9 @@
</li> </li>
--> -->
<li> <li>
<a href="../zh" title="Ghostfolio in Chinese">Chinese</a> <a href="../zh" title="Ghostfolio in Chinese (简体中文)"
>Chinese (简体中文)</a
>
</li> </li>
<li> <li>
<a href="../de" title="Ghostfolio in Deutsch">Deutsch</a> <a href="../de" title="Ghostfolio in Deutsch">Deutsch</a>
@ -139,6 +141,13 @@
<li> <li>
<a href="../it" title="Ghostfolio in Italiano">Italiano</a> <a href="../it" title="Ghostfolio in Italiano">Italiano</a>
</li> </li>
<!--
<li>
<a href="../ko" title="Ghostfolio in Korean (한국어)"
>Korean (한국어)</a
>
</li>
-->
<li> <li>
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a> <a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
</li> </li>
@ -153,7 +162,9 @@
</li> </li>
<!-- <!--
<li> <li>
<a href="../uk" title="Ghostfolio in Українська">Українська</a> <a href="../uk" title="Ghostfolio in Ukrainian (Українська)"
>Ukrainian (Українська)</a
>
</li> </li>
--> -->
</ul> </ul>

60
apps/client/src/app/components/header/header.component.html

@ -2,7 +2,7 @@
@if (user) { @if (user) {
<div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }"> <div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }">
<a <a
class="align-items-center justify-content-start rounded-0" class="align-items-center h-100 justify-content-start rounded-0"
mat-button mat-button
[ngClass]="{ 'w-100': hasTabs }" [ngClass]="{ 'w-100': hasTabs }"
[routerLink]="['/']" [routerLink]="['/']"
@ -15,9 +15,9 @@
<ul class="align-items-center d-flex list-inline m-0 px-2"> <ul class="align-items-center d-flex list-inline m-0 px-2">
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block rounded"
i18n i18n
mat-flat-button mat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': 'font-weight-bold':
currentRoute === internalRoutes.home.path || currentRoute === internalRoutes.home.path ||
@ -32,9 +32,9 @@
</li> </li>
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block rounded"
i18n i18n
mat-flat-button mat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === internalRoutes.portfolio.path, 'font-weight-bold': currentRoute === internalRoutes.portfolio.path,
'text-decoration-underline': 'text-decoration-underline':
@ -46,9 +46,9 @@
</li> </li>
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block rounded"
i18n i18n
mat-flat-button mat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === internalRoutes.accounts.path, 'font-weight-bold': currentRoute === internalRoutes.accounts.path,
'text-decoration-underline': 'text-decoration-underline':
@ -61,9 +61,9 @@
@if (hasPermissionToAccessAdminControl) { @if (hasPermissionToAccessAdminControl) {
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block rounded"
i18n i18n
mat-flat-button mat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': 'font-weight-bold':
currentRoute === internalRoutes.adminControl.path, currentRoute === internalRoutes.adminControl.path,
@ -77,9 +77,9 @@
} }
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block rounded"
i18n i18n
mat-flat-button mat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === routeResources, 'font-weight-bold': currentRoute === routeResources,
'text-decoration-underline': currentRoute === routeResources 'text-decoration-underline': currentRoute === routeResources
@ -93,8 +93,8 @@
) { ) {
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block rounded"
mat-flat-button mat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === routePricing, 'font-weight-bold': currentRoute === routePricing,
'text-decoration-underline': currentRoute === routePricing 'text-decoration-underline': currentRoute === routePricing
@ -112,9 +112,9 @@
} }
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block rounded"
i18n i18n
mat-flat-button mat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === routeAbout, 'font-weight-bold': currentRoute === routeAbout,
'text-decoration-underline': currentRoute === routeAbout 'text-decoration-underline': currentRoute === routeAbout
@ -164,7 +164,7 @@
<li class="list-inline-item"> <li class="list-inline-item">
<button <button
class="no-min-width px-1" class="no-min-width px-1"
mat-flat-button mat-button
[matMenuTriggerFor]="accountMenu" [matMenuTriggerFor]="accountMenu"
(menuClosed)="onMenuClosed()" (menuClosed)="onMenuClosed()"
(menuOpened)="onMenuOpened()" (menuOpened)="onMenuOpened()"
@ -334,7 +334,7 @@
@if (user === null) { @if (user === null) {
<div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }"> <div class="d-flex h-100 logo-container" [ngClass]="{ filled: hasTabs }">
<a <a
class="align-items-center justify-content-start rounded-0" class="align-items-center h-100 justify-content-start rounded-0"
mat-button mat-button
[ngClass]="{ 'w-100': hasTabs }" [ngClass]="{ 'w-100': hasTabs }"
[routerLink]="['/']" [routerLink]="['/']"
@ -350,9 +350,9 @@
<ul class="align-items-center d-flex list-inline m-0 px-2"> <ul class="align-items-center d-flex list-inline m-0 px-2">
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block rounded"
i18n i18n
mat-flat-button mat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === routeFeatures, 'font-weight-bold': currentRoute === routeFeatures,
'text-decoration-underline': currentRoute === routeFeatures 'text-decoration-underline': currentRoute === routeFeatures
@ -363,9 +363,9 @@
</li> </li>
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block rounded"
i18n i18n
mat-flat-button mat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === routeAbout, 'font-weight-bold': currentRoute === routeAbout,
'text-decoration-underline': currentRoute === routeAbout 'text-decoration-underline': currentRoute === routeAbout
@ -377,8 +377,8 @@
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-sm-block" class="d-sm-block rounded"
mat-flat-button mat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === routePricing, 'font-weight-bold': currentRoute === routePricing,
'text-decoration-underline': currentRoute === routePricing 'text-decoration-underline': currentRoute === routePricing
@ -397,9 +397,9 @@
@if (hasPermissionToAccessFearAndGreedIndex) { @if (hasPermissionToAccessFearAndGreedIndex) {
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block rounded"
i18n i18n
mat-flat-button mat-button
[ngClass]="{ [ngClass]="{
'font-weight-bold': currentRoute === routeMarkets, 'font-weight-bold': currentRoute === routeMarkets,
'text-decoration-underline': currentRoute === routeMarkets 'text-decoration-underline': currentRoute === routeMarkets
@ -413,19 +413,23 @@
<a <a
class="d-none d-sm-block no-min-width p-1" class="d-none d-sm-block no-min-width p-1"
href="https://github.com/ghostfolio/ghostfolio" href="https://github.com/ghostfolio/ghostfolio"
mat-flat-button mat-button
><ion-icon name="logo-github" ><ion-icon name="logo-github"
/></a> /></a>
</li> </li>
<li class="list-inline-item"> <li class="list-inline-item">
<button class="d-sm-block" mat-flat-button (click)="openLoginDialog()"> <button
class="d-sm-block rounded"
mat-button
(click)="openLoginDialog()"
>
<ng-container i18n>Sign in</ng-container> <ng-container i18n>Sign in</ng-container>
</button> </button>
</li> </li>
@if (currentRoute !== 'register' && hasPermissionToCreateUser) { @if (currentRoute !== 'register' && hasPermissionToCreateUser) {
<li class="list-inline-item ml-1"> <li class="list-inline-item ml-1">
<a <a
class="d-none d-sm-block" class="d-none d-sm-block px-3 rounded"
color="primary" color="primary"
i18n i18n
mat-flat-button mat-flat-button

2
apps/client/src/app/components/header/header.component.scss

@ -24,7 +24,7 @@
} }
.mdc-button { .mdc-button {
height: 100%; color: var(--mdc-text-button-label-text-color);
&:not(.mat-primary) { &:not(.mat-primary) {
background-color: transparent; background-color: transparent;

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;
} }

4
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>
@ -400,7 +400,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"

1
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts

@ -89,6 +89,7 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
'es', 'es',
'fr', 'fr',
'it', 'it',
'ko',
'nl', 'nl',
'pl', 'pl',
'pt', 'pt',

13
apps/client/src/app/components/user-account-settings/user-account-settings.html

@ -87,7 +87,8 @@
> >
} }
<mat-option value="zh" <mat-option value="zh"
>Chinese (<ng-container i18n>Community</ng-container >Chinese / 简体中文 (<ng-container i18n
>Community</ng-container
>)</mat-option >)</mat-option
> >
<mat-option value="es" <mat-option value="es"
@ -102,6 +103,13 @@
>Italiano (<ng-container i18n>Community</ng-container >Italiano (<ng-container i18n>Community</ng-container
>)</mat-option >)</mat-option
> >
@if (user?.settings?.isExperimentalFeatures) {
<mat-option value="ko"
>Korean / 한국어 (<ng-container i18n
>Community</ng-container
>)</mat-option
>
}
<mat-option value="nl" <mat-option value="nl"
>Nederlands (<ng-container i18n>Community</ng-container >Nederlands (<ng-container i18n>Community</ng-container
>)</mat-option >)</mat-option
@ -120,7 +128,8 @@
> >
@if (user?.settings?.isExperimentalFeatures) { @if (user?.settings?.isExperimentalFeatures) {
<mat-option value="uk" <mat-option value="uk"
>Українська (<ng-container i18n>Community</ng-container >Ukrainian / Українська (<ng-container i18n
>Community</ng-container
>)</mat-option >)</mat-option
> >
} }

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.testmu.ai?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 === 'LIGHT'
? '../assets/images/sponsors/logo-testmu-dark.svg'
: '../assets/images/sponsors/logo-testmu-light.svg'
"
/> />
</a> </a>
</div> </div>

5
apps/client/src/app/pages/features/features-page.html

@ -260,8 +260,9 @@
<p class="m-0"> <p class="m-0">
Use Ghostfolio in multiple languages: English, Use Ghostfolio in multiple languages: English,
<!-- Català, --> <!-- Català, -->
Chinese, Dutch, French, German, Italian, Polish, Portuguese, Chinese, Dutch, French, German, Italian,
Spanish and Turkish <!-- Korean, -->
Polish, Portuguese, Spanish and Turkish
<!-- and Ukrainian --> <!-- and Ukrainian -->
are currently supported. are currently supported.
</p> </p>

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'

5
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts

@ -43,15 +43,16 @@ export class GfProductPageComponent implements OnInit {
isOpenSource: true, isOpenSource: true,
key: 'ghostfolio', key: 'ghostfolio',
languages: [ languages: [
'Chinese (简体中文)',
'Deutsch', 'Deutsch',
'English', 'English',
'Español', 'Español',
'Français', 'Français',
'Italiano', 'Italiano',
'Korean (한국어)',
'Nederlands', 'Nederlands',
'Português', 'Português',
'Türkçe', 'Türkçe'
'简体中文'
], ],
name: 'Ghostfolio', name: 'Ghostfolio',
origin: $localize`Switzerland`, origin: $localize`Switzerland`,

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: ''
}; };
/* /*

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

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

File diff suppressed because it is too large

83
apps/client/src/styles.scss

@ -1,3 +1,5 @@
@use '@angular/material' as mat;
@import './styles/bootstrap'; @import './styles/bootstrap';
@import './styles/table'; @import './styles/table';
@import './styles/variables'; @import './styles/variables';
@ -256,46 +258,10 @@ body {
} }
} }
.mat-mdc-card {
--mat-card-elevated-container-color: var(--dark-background);
--mat-card-outlined-container-color: var(--dark-background);
}
.mat-mdc-fab {
&.mat-primary {
--mat-fab-icon-color: rgba(var(--dark-primary-text));
--mat-mdc-fab-color: rgba(var(--dark-primary-text));
}
}
.mat-mdc-paginator { .mat-mdc-paginator {
background-color: rgba(var(--palette-foreground-base-dark), 0.02); background-color: rgba(var(--palette-foreground-base-dark), 0.02);
} }
.mat-mdc-slide-toggle {
.mdc-switch__track {
--mat-slide-toggle-selected-focus-track-color: rgba(
255,
255,
255,
0.12
);
--mat-slide-toggle-selected-hover-track-color: rgba(
255,
255,
255,
0.12
);
--mat-slide-toggle-selected-pressed-track-color: rgba(
255,
255,
255,
0.12
);
--mat-slide-toggle-selected-track-color: rgba(255, 255, 255, 0.12);
}
}
.mdc-button { .mdc-button {
&.mat-accent, &.mat-accent,
&.mat-primary { &.mat-primary {
@ -305,10 +271,6 @@ body {
.page { .page {
&.has-tabs { &.has-tabs {
.mat-mdc-tab-nav-bar {
--mat-tab-inactive-label-text-color: rgba(var(--light-primary-text));
}
@media (min-width: 576px) { @media (min-width: 576px) {
.mat-mdc-tab-header { .mat-mdc-tab-header {
background-color: rgba(var(--palette-foreground-base-dark), 0.02); background-color: rgba(var(--palette-foreground-base-dark), 0.02);
@ -443,8 +405,6 @@ ngx-skeleton-loader {
.mat-mdc-card { .mat-mdc-card {
.mat-mdc-card-title { .mat-mdc-card-title {
--mat-card-title-text-line-height: 1.2;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
} }
@ -468,15 +428,6 @@ ngx-skeleton-loader {
} }
} }
.mat-mdc-fab {
color: var(--mat-mdc-fab-color, inherit) !important;
&.mat-primary {
--mat-fab-icon-color: rgba(var(--light-primary-text));
--mat-mdc-fab-color: rgba(var(--light-primary-text));
}
}
.mat-mdc-form-field { .mat-mdc-form-field {
&.without-hint { &.without-hint {
.mat-mdc-form-field-subscript-wrapper { .mat-mdc-form-field-subscript-wrapper {
@ -517,15 +468,6 @@ ngx-skeleton-loader {
} }
} }
.mat-mdc-slide-toggle {
.mdc-switch__track {
--mat-slide-toggle-selected-focus-track-color: rgba(0, 0, 0, 0.12);
--mat-slide-toggle-selected-hover-track-color: rgba(0, 0, 0, 0.12);
--mat-slide-toggle-selected-pressed-track-color: rgba(0, 0, 0, 0.12);
--mat-slide-toggle-selected-track-color: rgba(0, 0, 0, 0.12);
}
}
.mat-stepper-vertical, .mat-stepper-vertical,
.mat-stepper-horizontal { .mat-stepper-horizontal {
background: transparent !important; background: transparent !important;
@ -583,32 +525,23 @@ ngx-skeleton-loader {
} }
} }
.mat-mdc-tab-nav-bar {
--mat-tab-active-focus-indicator-color: transparent;
--mat-tab-active-hover-indicator-color: transparent;
--mat-tab-inactive-label-text-color: rgba(var(--dark-primary-text));
--mat-tab-active-indicator-color: transparent;
}
.mat-mdc-tab-nav-panel { .mat-mdc-tab-nav-panel {
padding: 2rem 0; padding: 2rem 0;
} }
@media (max-width: 575.98px) {
.mat-mdc-tab-link {
--mat-tab-container-height: 3rem;
}
}
@media (min-width: 576px) { @media (min-width: 576px) {
flex-direction: row-reverse; flex-direction: row-reverse;
@include mat.tabs-overrides(
(
divider-height: 0
)
);
.mat-mdc-tab-header { .mat-mdc-tab-header {
background-color: rgba(var(--palette-foreground-base), 0.02); background-color: rgba(var(--palette-foreground-base), 0.02);
padding: 2rem 0; padding: 2rem 0;
width: 14rem; width: 14rem;
--mat-tab-label-text-tracking: normal;
--mat-tab-container-height: 2rem;
.mat-mdc-tab-links { .mat-mdc-tab-links {
flex-direction: column; flex-direction: column;

155
apps/client/src/styles/m3-theme.scss

@ -0,0 +1,155 @@
// This file was generated by running 'ng generate @angular/material:m3-theme'.
// Proceed with caution if making changes to this file.
@use 'sass:map';
@use '@angular/material' as mat;
// Note: Color palettes are generated from primary: #36cfcc, secondary: #3686cf
$_palettes: (
primary: (
0: #000000,
10: #00201f,
20: #003736,
25: #004342,
30: #00504e,
35: #005d5b,
40: #006a68,
50: #008583,
60: #00a19f,
70: #11bebc,
80: #47dbd7,
90: #6bf7f4,
95: #affffc,
98: #e3fffd,
99: #f2fffe,
100: #ffffff
),
secondary: (
0: #000000,
10: #001d36,
20: #003258,
25: #003d6a,
30: #00497c,
35: #00558f,
40: #0061a3,
50: #267bc3,
60: #4895df,
70: #66b0fb,
80: #9ecaff,
90: #d1e4ff,
95: #e9f1ff,
98: #f8f9ff,
99: #fdfcff,
100: #ffffff
),
tertiary: (
0: #000000,
10: #031d35,
20: #1b324b,
25: #273d57,
30: #324863,
35: #3e546f,
40: #4a607b,
50: #637995,
60: #7c92b0,
70: #97adcc,
80: #b2c8e8,
90: #d2e4ff,
95: #eaf1ff,
98: #f8f9ff,
99: #fdfcff,
100: #ffffff
),
neutral: (
0: #000000,
10: #191c1c,
20: #2d3131,
25: #383c3c,
30: #444747,
35: #4f5353,
40: #5b5f5e,
50: #747877,
60: #8e9191,
70: #a9acab,
80: #c4c7c6,
90: #e0e3e2,
95: #eff1f0,
98: #f7faf9,
99: #fafdfc,
100: #ffffff,
4: #0b0f0f,
6: #101414,
12: #1d2020,
17: #272b2a,
22: #323535,
24: #363a39,
87: #d8dada,
92: #e6e9e8,
94: #eceeed,
96: #f2f4f3
),
neutral-variant: (
0: #000000,
10: #141d1d,
20: #293232,
25: #343d3d,
30: #3f4948,
35: #4a5454,
40: #566060,
50: #6f7978,
60: #889392,
70: #a3adac,
80: #bec9c7,
90: #dae5e3,
95: #e8f3f2,
98: #f1fbfa,
99: #f4fefd,
100: #ffffff
),
error: (
0: #000000,
10: #410002,
20: #690005,
25: #7e0007,
30: #93000a,
35: #a80710,
40: #ba1a1a,
50: #de3730,
60: #ff5449,
70: #ff897d,
80: #ffb4ab,
90: #ffdad6,
95: #ffedea,
98: #fff8f7,
99: #fffbff,
100: #ffffff
)
);
$_rest: (
secondary: map.get($_palettes, secondary),
neutral: map.get($_palettes, neutral),
neutral-variant: map.get($_palettes, neutral-variant),
error: map.get($_palettes, error)
);
$_primary: map.merge(map.get($_palettes, primary), $_rest);
$_tertiary: map.merge(map.get($_palettes, tertiary), $_rest);
$light-theme: mat.define-theme(
(
color: (
theme-type: light,
primary: $_primary,
tertiary: $_tertiary
)
)
);
$dark-theme: mat.define-theme(
(
color: (
theme-type: dark,
primary: $_primary,
tertiary: $_tertiary
)
)
);

506
apps/client/src/styles/theme.scss

@ -1,109 +1,427 @@
@use '@angular/material' as mat; @use '@angular/material' as mat;
@use 'sass:map';
@use './variables.scss' as variables; $dark-primary-text: rgba(black, 0.87);
$light-primary-text: white;
$gf-primary: (
50: var(--gf-theme-primary-50),
100: var(--gf-theme-primary-100),
200: var(--gf-theme-primary-200),
300: var(--gf-theme-primary-300),
400: var(--gf-theme-primary-400),
500: var(--gf-theme-primary-500),
600: var(--gf-theme-primary-600),
700: var(--gf-theme-primary-700),
800: var(--gf-theme-primary-800),
900: var(--gf-theme-primary-900),
A100: var(--gf-theme-primary-A100),
A200: var(--gf-theme-primary-A200),
A400: var(--gf-theme-primary-A400),
A700: var(--gf-theme-primary-A700),
contrast: (
50: variables.$dark-primary-text,
100: variables.$dark-primary-text,
200: variables.$dark-primary-text,
300: variables.$light-primary-text,
400: variables.$light-primary-text,
500: variables.$light-primary-text,
600: variables.$light-primary-text,
700: variables.$light-primary-text,
800: variables.$light-primary-text,
900: variables.$light-primary-text,
A100: variables.$dark-primary-text,
A200: variables.$light-primary-text,
A400: variables.$light-primary-text,
A700: variables.$light-primary-text
)
);
$gf-secondary: ( $_palettes: (
50: var(--gf-theme-secondary-50), primary: (
100: var(--gf-theme-secondary-100), 0: #000000,
200: var(--gf-theme-secondary-200), 10: #00201f,
300: var(--gf-theme-secondary-300), 20: #003736,
400: var(--gf-theme-secondary-400), 25: #004342,
500: var(--gf-theme-secondary-500), 30: #00504e,
600: var(--gf-theme-secondary-600), 35: #005d5b,
700: var(--gf-theme-secondary-700), 40: #006a68,
800: var(--gf-theme-secondary-800), 50: #008583,
900: var(--gf-theme-secondary-900), 60: #00a19f,
A100: var(--gf-theme-secondary-A100), 70: #11bebc,
A200: var(--gf-theme-secondary-A200), 80: #47dbd7,
A400: var(--gf-theme-secondary-A400), 90: #6bf7f4,
A700: var(--gf-theme-secondary-A700), 95: #affffc,
contrast: ( 98: #e3fffd,
50: variables.$dark-primary-text, 99: #f2fffe,
100: variables.$dark-primary-text, 100: #ffffff
200: variables.$dark-primary-text, ),
300: variables.$light-primary-text, secondary: (
400: variables.$light-primary-text, 0: #000000,
500: variables.$light-primary-text, 10: #001d36,
600: variables.$light-primary-text, 20: #003258,
700: variables.$light-primary-text, 25: #003d6a,
800: variables.$light-primary-text, 30: #00497c,
900: variables.$light-primary-text, 35: #00558f,
A100: variables.$dark-primary-text, 40: #0061a3,
A200: variables.$light-primary-text, 50: #267bc3,
A400: variables.$light-primary-text, 60: #4895df,
A700: variables.$light-primary-text 70: #66b0fb,
80: #9ecaff,
90: #d1e4ff,
95: #e9f1ff,
98: #f8f9ff,
99: #fdfcff,
100: #ffffff
),
tertiary: (
0: #000000,
10: #031d35,
20: #1b324b,
25: #273d57,
30: #324863,
35: #3e546f,
40: #4a607b,
50: #637995,
60: #7c92b0,
70: #97adcc,
80: #b2c8e8,
90: #d2e4ff,
95: #eaf1ff,
98: #f8f9ff,
99: #fdfcff,
100: #ffffff
),
neutral: (
0: #000000,
10: #191c1c,
20: #2d3131,
25: #383c3c,
30: #444747,
35: #4f5353,
40: #5b5f5e,
50: #747877,
60: #8e9191,
70: #a9acab,
80: #c4c7c6,
90: #e0e3e2,
95: #eff1f0,
98: #f7faf9,
99: #fafdfc,
100: #ffffff,
4: #0b0f0f,
6: #101414,
12: #1d2020,
17: #272b2a,
22: #323535,
24: #363a39,
87: #d8dada,
92: #e6e9e8,
94: #eceeed,
96: #f2f4f3
),
neutral-variant: (
0: #000000,
10: #141d1d,
20: #293232,
25: #343d3d,
30: #3f4948,
35: #4a5454,
40: #566060,
50: #6f7978,
60: #889392,
70: #a3adac,
80: #bec9c7,
90: #dae5e3,
95: #e8f3f2,
98: #f1fbfa,
99: #f4fefd,
100: #ffffff
),
error: (
0: #000000,
10: #410002,
20: #690005,
25: #7e0007,
30: #93000a,
35: #a80710,
40: #ba1a1a,
50: #de3730,
60: #ff5449,
70: #ff897d,
80: #ffb4ab,
90: #ffdad6,
95: #ffedea,
98: #fff8f7,
99: #fffbff,
100: #ffffff
) )
); );
$gf-typography: mat.m2-define-typography-config(); $_rest: (
secondary: map.get($_palettes, secondary),
// Create default theme neutral: map.get($_palettes, neutral),
$gf-theme-default: mat.m2-define-light-theme( neutral-variant: map.get($_palettes, neutral-variant),
( error: map.get($_palettes, error)
color: (
accent: mat.m2-define-palette($gf-secondary, 500, 900, A100),
primary: mat.m2-define-palette($gf-primary)
),
density: -3,
typography: $gf-typography
)
); );
$_primary: map.merge(map.get($_palettes, primary), $_rest);
$_tertiary: map.merge(map.get($_palettes, tertiary), $_rest);
@include mat.all-component-themes($gf-theme-default); @include mat.app-background();
@include mat.button-density(0);
// Create dark theme @include mat.elevation-classes();
$gf-theme-dark: mat.m2-define-dark-theme( @include mat.table-density(-1);
(
color: ( // $gf-typography: mat.m2-define-typography-config();
accent: mat.m2-define-palette($gf-secondary, 500, 900, A100),
primary: mat.m2-define-palette($gf-primary) .theme-light {
), $gf-theme-default: mat.define-theme(
density: -3, (
typography: $gf-typography color: (
) primary: $_primary,
); theme-type: light,
tertiary: $_tertiary
),
density: (
scale: -3
),
// typography: $gf-typography
)
);
@include mat.all-component-themes($gf-theme-default);
@include mat.autocomplete-overrides(
(
background-color: var(--light-background)
)
);
@include mat.button-overrides(
(
outlined-label-text-color: var(--dark-primary-text),
text-label-text-color: var(--dark-primary-text)
)
);
@include mat.button-toggle-overrides(
(
selected-state-background-color: rgba(var(--dark-dividers)),
selected-state-text-color: var(--dark-primary-text)
)
);
@include mat.card-overrides(
(
outlined-container-color: var(--light-background),
outlined-outline-color: rgba(var(--dark-dividers)),
title-text-line-height: 1.2
)
);
@include mat.datepicker-overrides(
(
calendar-container-background-color: var(--light-background)
)
);
@include mat.dialog-overrides(
(
container-color: var(--light-background)
)
);
@include mat.menu-overrides(
(
container-color: var(--light-background)
)
);
@include mat.select-overrides(
(
panel-background-color: var(--light-background)
)
);
@include mat.slide-toggle-overrides(
(
selected-track-outline-color: rgba(var(--dark-dividers)),
track-outline-color: rgba(var(--dark-dividers))
)
);
@include mat.table-overrides(
(
row-item-outline-color: rgba(var(--dark-dividers))
)
);
}
.theme-dark { .theme-dark {
@include mat.all-component-colors($gf-theme-dark); $gf-theme-dark: mat.define-theme(
(
color: (
primary: $_primary,
theme-type: dark,
tertiary: $_tertiary
),
density: (
scale: -3
),
// typography: $gf-typography
)
);
@include mat.all-component-themes($gf-theme-dark);
@include mat.button-overrides(
(
outlined-label-text-color: var(--light-primary-text),
text-label-text-color: var(--light-primary-text)
)
);
@include mat.button-toggle-overrides(
(
selected-state-background-color: rgba(var(--light-dividers)),
selected-state-text-color: var(--light-primary-text)
)
);
@include mat.card-overrides(
(
outlined-container-color: var(--dark-background),
outlined-outline-color: rgba(var(--light-dividers)),
title-text-line-height: 1.2
)
);
@include mat.slide-toggle-overrides(
(
selected-track-outline-color: rgba(var(--light-dividers)),
track-outline-color: rgba(var(--light-dividers))
)
);
@include mat.table-overrides(
(
row-item-outline-color: rgba(var(--light-dividers))
)
);
} }
@include mat.button-density(0); .theme-dark,
@include mat.elevation-classes(); .theme-light {
@include mat.app-background(); @media (max-width: 575.98px) {
@include mat.table-density(-1); @include mat.dialog-overrides(
(
container-shape: 4px
)
);
.page.has-tabs {
@include mat.tabs-overrides(
(
container-height: 3rem
)
);
}
}
@media (min-width: 576px) {
.page.has-tabs {
@include mat.tabs-overrides(
(
container-height: 2rem
)
);
}
}
@include mat.button-overrides(
(
filled-container-color: var(--gf-theme-primary-500)
)
);
@include mat.checkbox-overrides(
(
selected-icon-color: var(--gf-theme-primary-500)
)
);
@include mat.datepicker-overrides(
(
calendar-container-elevation-shadow: var(
--mat-select-container-elevation-shadow
),
calendar-date-selected-state-background-color: var(--gf-theme-primary-500),
calendar-date-today-selected-state-outline-color: var(
--gf-theme-primary-500
)
)
);
@include mat.dialog-overrides(
(
container-max-width: 80vw,
container-small-max-width: 96vw
)
);
@include mat.fab-overrides(
(
container-color: var(--gf-theme-primary-500)
)
);
@include mat.form-field-overrides(
(
outlined-focus-label-text-color: var(--gf-theme-primary-500),
outlined-focus-outline-color: var(--gf-theme-primary-500)
)
);
@include mat.progress-bar-overrides(
(
active-indicator-color: var(--gf-theme-primary-500)
)
);
@include mat.slide-toggle-overrides(
(
selected-focus-handle-color: var(--gf-theme-primary-500),
selected-focus-track-color: transparent,
selected-handle-color: var(--gf-theme-primary-500),
selected-hover-handle-color: var(--gf-theme-primary-500),
selected-hover-track-color: transparent,
selected-pressed-handle-color: var(--gf-theme-primary-500),
selected-pressed-track-color: transparent,
selected-track-color: transparent,
unselected-hover-track-color: transparent,
unselected-track-color: transparent
)
);
@include mat.slider-overrides(
(
active-track-color: var(--gf-theme-primary-500),
focus-handle-color: var(--gf-theme-primary-500),
handle-color: var(--gf-theme-primary-500)
)
);
@include mat.stepper-overrides(
(
header-selected-state-icon-background-color: var(--gf-theme-primary-500)
)
);
@include mat.tabs-overrides(
(
active-focus-label-text-color: var(--gf-theme-primary-500),
active-hover-label-text-color: var(--gf-theme-primary-500),
active-label-text-color: var(--gf-theme-primary-500),
active-ripple-color: var(--gf-theme-primary-500),
inactive-ripple-color: var(--gf-theme-primary-500)
)
);
.mat-accent {
@include mat.button-overrides(
(
filled-container-color: var(--gf-theme-secondary-500),
outlined-label-text-color: var(--gf-theme-secondary-500)
)
);
}
.mat-warn {
@include mat.button-overrides(
(
filled-container-color: #f44336,
filled-label-text-color: white,
outlined-label-text-color: #f44336
)
);
}
.page.has-tabs {
@include mat.tabs-overrides(
(
active-indicator-height: 0,
label-text-tracking: normal
)
);
}
}
:root { :root {
--gf-theme-alpha-hover: 0.04; --gf-theme-alpha-hover: 0.04;

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

@ -193,6 +193,7 @@ export const SUPPORTED_LANGUAGE_CODES = [
'es', 'es',
'fr', 'fr',
'it', 'it',
'ko',
'nl', 'nl',
'pl', 'pl',
'pt', 'pt',

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

@ -11,7 +11,21 @@ import {
parseISO, parseISO,
subDays subDays
} from 'date-fns'; } from 'date-fns';
import { ca, de, es, fr, it, nl, pl, pt, tr, uk, zhCN } from 'date-fns/locale'; import {
ca,
de,
es,
fr,
it,
ko,
nl,
pl,
pt,
tr,
uk,
zhCN
} from 'date-fns/locale';
import { get, isNil, isString } from 'lodash';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
@ -184,6 +198,8 @@ export function getDateFnsLocale(aLanguageCode: string) {
return fr; return fr;
} else if (aLanguageCode === 'it') { } else if (aLanguageCode === 'it') {
return it; return it;
} else if (aLanguageCode === 'ko') {
return ko;
} else if (aLanguageCode === 'nl') { } else if (aLanguageCode === 'nl') {
return nl; return nl;
} else if (aLanguageCode === 'pl') { } else if (aLanguageCode === 'pl') {
@ -242,6 +258,16 @@ export function getLocale() {
return navigator.language ?? locale; return navigator.language ?? locale;
} }
export function getLowercase(object: object, path: string) {
const value = get(object, path);
if (isNil(value)) {
return '';
}
return isString(value) ? value.toLocaleLowerCase() : value;
}
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);

2
libs/common/src/lib/interfaces/index.ts

@ -34,7 +34,6 @@ import type { PortfolioPerformance } from './portfolio-performance.interface';
import type { PortfolioPosition } from './portfolio-position.interface'; import type { PortfolioPosition } from './portfolio-position.interface';
import type { PortfolioReportRule } from './portfolio-report-rule.interface'; import type { PortfolioReportRule } from './portfolio-report-rule.interface';
import type { PortfolioSummary } from './portfolio-summary.interface'; import type { PortfolioSummary } from './portfolio-summary.interface';
import type { Position } from './position.interface';
import type { Product } from './product'; import type { Product } from './product';
import type { AccessTokenResponse } from './responses/access-token-response.interface'; import type { AccessTokenResponse } from './responses/access-token-response.interface';
import type { AccountBalancesResponse } from './responses/account-balances-response.interface'; import type { AccountBalancesResponse } from './responses/account-balances-response.interface';
@ -172,7 +171,6 @@ export {
PortfolioReportResponse, PortfolioReportResponse,
PortfolioReportRule, PortfolioReportRule,
PortfolioSummary, PortfolioSummary,
Position,
Product, Product,
PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON, PublicKeyCredentialRequestOptionsJSON,

6
libs/common/src/lib/interfaces/info-item.interface.ts

@ -1,4 +1,4 @@
import { Platform, SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
import { Statistics } from './statistics.interface'; import { Statistics } from './statistics.interface';
import { SubscriptionOffer } from './subscription-offer.interface'; import { SubscriptionOffer } from './subscription-offer.interface';
@ -13,10 +13,6 @@ export interface InfoItem {
globalPermissions: string[]; globalPermissions: string[];
isDataGatheringEnabled?: string; isDataGatheringEnabled?: string;
isReadOnlyMode?: boolean; isReadOnlyMode?: boolean;
/** @deprecated */
platforms: Platform[];
statistics: Statistics; statistics: Statistics;
subscriptionOffer?: SubscriptionOffer; subscriptionOffer?: SubscriptionOffer;
} }

4
libs/common/src/lib/interfaces/portfolio-position.interface.ts

@ -7,6 +7,7 @@ import { Holding } from './holding.interface';
import { Sector } from './sector.interface'; import { Sector } from './sector.interface';
export interface PortfolioPosition { export interface PortfolioPosition {
activitiesCount: number;
allocationInPercentage: number; allocationInPercentage: number;
assetClass?: AssetClass; assetClass?: AssetClass;
assetClassLabel?: string; assetClassLabel?: string;
@ -38,7 +39,10 @@ export interface PortfolioPosition {
sectors: Sector[]; sectors: Sector[];
symbol: string; symbol: string;
tags?: Tag[]; tags?: Tag[];
/** @deprecated use activitiesCount instead */
transactionCount: number; transactionCount: number;
type?: string; type?: string;
url?: string; url?: string;
valueInBaseCurrency?: number; valueInBaseCurrency?: number;

27
libs/common/src/lib/interfaces/position.interface.ts

@ -1,27 +0,0 @@
import { MarketState } from '@ghostfolio/common/types';
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
export interface Position {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
averagePrice: number;
currency: string;
dataSource: DataSource;
firstBuyDate: string;
grossPerformance?: number;
grossPerformancePercentage?: number;
investment: number;
investmentInOriginalCurrency?: number;
marketPrice?: number;
marketState?: MarketState;
name?: string;
netPerformance?: number;
netPerformancePercentage?: number;
netPerformancePercentageWithCurrencyEffect?: number;
netPerformanceWithCurrencyEffect?: number;
quantity: number;
symbol: string;
transactionCount: number;
url?: string;
}

3
libs/common/src/lib/interfaces/responses/accounts-response.interface.ts

@ -2,9 +2,12 @@ import { AccountWithValue } from '@ghostfolio/common/types';
export interface AccountsResponse { export interface AccountsResponse {
accounts: AccountWithValue[]; accounts: AccountWithValue[];
activitiesCount: number;
totalBalanceInBaseCurrency: number; totalBalanceInBaseCurrency: number;
totalDividendInBaseCurrency: number; totalDividendInBaseCurrency: number;
totalInterestInBaseCurrency: number; totalInterestInBaseCurrency: number;
totalValueInBaseCurrency: number; totalValueInBaseCurrency: number;
/** @deprecated use activitiesCount instead */
transactionCount: number; transactionCount: number;
} }

6
libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts

@ -1,5 +1,4 @@
import { import {
Activity,
Benchmark, Benchmark,
DataProviderInfo, DataProviderInfo,
EnhancedSymbolProfile, EnhancedSymbolProfile,
@ -9,17 +8,14 @@ import {
import { Tag } from '@prisma/client'; import { Tag } from '@prisma/client';
export interface PortfolioHoldingResponse { export interface PortfolioHoldingResponse {
/** @deprecated */
activities: Activity[];
activitiesCount: number; activitiesCount: number;
averagePrice: number; averagePrice: number;
dataProviderInfo: DataProviderInfo; dataProviderInfo: DataProviderInfo;
dateOfFirstActivity: string;
dividendInBaseCurrency: number; dividendInBaseCurrency: number;
dividendYieldPercent: number; dividendYieldPercent: number;
dividendYieldPercentWithCurrencyEffect: number; dividendYieldPercentWithCurrencyEffect: number;
feeInBaseCurrency: number; feeInBaseCurrency: number;
firstBuyDate: string;
grossPerformance: number; grossPerformance: number;
grossPerformancePercent: number; grossPerformancePercent: number;
grossPerformancePercentWithCurrencyEffect: number; grossPerformancePercentWithCurrencyEffect: number;

7
libs/common/src/lib/models/timeline-position.ts

@ -9,12 +9,15 @@ import { Big } from 'big.js';
import { Transform, Type } from 'class-transformer'; import { Transform, Type } from 'class-transformer';
export class TimelinePosition { export class TimelinePosition {
activitiesCount: number;
@Transform(transformToBig, { toClassOnly: true }) @Transform(transformToBig, { toClassOnly: true })
@Type(() => Big) @Type(() => Big)
averagePrice: Big; averagePrice: Big;
currency: string; currency: string;
dataSource: DataSource; dataSource: DataSource;
dateOfFirstActivity: string;
@Transform(transformToBig, { toClassOnly: true }) @Transform(transformToBig, { toClassOnly: true })
@Type(() => Big) @Type(() => Big)
@ -32,6 +35,7 @@ export class TimelinePosition {
@Type(() => Big) @Type(() => Big)
feeInBaseCurrency: Big; feeInBaseCurrency: Big;
/** @deprecated use dateOfFirstActivity instead */
firstBuyDate: string; firstBuyDate: string;
@Transform(transformToBig, { toClassOnly: true }) @Transform(transformToBig, { toClassOnly: true })
@ -50,6 +54,8 @@ export class TimelinePosition {
@Type(() => Big) @Type(() => Big)
grossPerformanceWithCurrencyEffect: Big; grossPerformanceWithCurrencyEffect: Big;
includeInTotalAssetValue?: boolean;
@Transform(transformToBig, { toClassOnly: true }) @Transform(transformToBig, { toClassOnly: true })
@Type(() => Big) @Type(() => Big)
investment: Big; investment: Big;
@ -90,6 +96,7 @@ export class TimelinePosition {
@Type(() => Big) @Type(() => Big)
timeWeightedInvestmentWithCurrencyEffect: Big; timeWeightedInvestmentWithCurrencyEffect: Big;
/** @deprecated use activitiesCount instead */
transactionCount: number; transactionCount: number;
@Transform(transformToBig, { toClassOnly: true }) @Transform(transformToBig, { toClassOnly: true })

4
libs/common/src/lib/types/account-with-value.type.ts

@ -1,12 +1,16 @@
import { Account as AccountModel, Platform } from '@prisma/client'; import { Account as AccountModel, Platform } from '@prisma/client';
export type AccountWithValue = AccountModel & { export type AccountWithValue = AccountModel & {
activitiesCount: number;
allocationInPercentage: number; allocationInPercentage: number;
balanceInBaseCurrency: number; balanceInBaseCurrency: number;
dividendInBaseCurrency: number; dividendInBaseCurrency: number;
interestInBaseCurrency: number; interestInBaseCurrency: number;
platform?: Platform; platform?: Platform;
/** @deprecated use activitiesCount instead */
transactionCount: number; transactionCount: number;
value: number; value: number;
valueInBaseCurrency: number; valueInBaseCurrency: number;
}; };

6
libs/ui/src/lib/accounts-table/accounts-table.component.ts

@ -1,5 +1,5 @@
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getLocale } from '@ghostfolio/common/helper'; import { getLocale, getLowercase } from '@ghostfolio/common/helper';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { NotificationService } from '@ghostfolio/ui/notifications'; import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
@ -32,7 +32,6 @@ import {
trashOutline, trashOutline,
walletOutline walletOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { get } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
@ -133,8 +132,9 @@ export class GfAccountsTableComponent implements OnChanges, OnDestroy {
this.isLoading = true; this.isLoading = true;
this.dataSource = new MatTableDataSource(this.accounts); this.dataSource = new MatTableDataSource(this.accounts);
this.dataSource.sortingDataAccessor = getLowercase;
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = get;
if (this.accounts) { if (this.accounts) {
this.isLoading = false; this.isLoading = false;

11
libs/ui/src/lib/benchmark/benchmark.component.ts

@ -1,5 +1,9 @@
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getLocale, resolveMarketCondition } from '@ghostfolio/common/helper'; import {
getLocale,
getLowercase,
resolveMarketCondition
} from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
Benchmark, Benchmark,
@ -28,7 +32,7 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { ellipsisHorizontal, trashOutline } from 'ionicons/icons'; import { ellipsisHorizontal, trashOutline } from 'ionicons/icons';
import { get, isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';
@ -111,8 +115,9 @@ export class GfBenchmarkComponent implements OnChanges, OnDestroy {
public ngOnChanges() { public ngOnChanges() {
if (this.benchmarks) { if (this.benchmarks) {
this.dataSource.data = this.benchmarks; this.dataSource.data = this.benchmarks;
this.dataSource.sortingDataAccessor = getLowercase;
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = get;
this.isLoading = false; this.isLoading = false;
} }

1
libs/ui/src/lib/environment/environment.interface.ts

@ -1,5 +1,4 @@
export interface GfEnvironment { export interface GfEnvironment {
lastPublish: string | null; lastPublish: string | null;
production: boolean; production: boolean;
stripePublicKey: string;
} }

7
libs/ui/src/lib/holdings-table/holdings-table.component.html

@ -19,12 +19,7 @@
</ng-container> </ng-container>
<ng-container matColumnDef="nameWithSymbol"> <ng-container matColumnDef="nameWithSymbol">
<th <th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="name">
*matHeaderCellDef
class="px-1"
mat-header-cell
mat-sort-header="symbol"
>
<ng-container i18n>Name</ng-container> <ng-container i18n>Name</ng-container>
</th> </th>
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell> <td *matCellDef="let element" class="line-height-1 px-1" mat-cell>

4
libs/ui/src/lib/holdings-table/holdings-table.component.ts

@ -1,4 +1,4 @@
import { getLocale } from '@ghostfolio/common/helper'; import { getLocale, getLowercase } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
PortfolioPosition PortfolioPosition
@ -92,6 +92,8 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy {
this.dataSource = new MatTableDataSource(this.holdings); this.dataSource = new MatTableDataSource(this.holdings);
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator;
this.dataSource.sortingDataAccessor = getLowercase;
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
if (this.holdings) { if (this.holdings) {

7
libs/ui/src/lib/mocks/holdings.ts

@ -2,6 +2,7 @@ import { PortfolioPosition } from '@ghostfolio/common/interfaces';
export const holdings: PortfolioPosition[] = [ export const holdings: PortfolioPosition[] = [
{ {
activitiesCount: 1,
allocationInPercentage: 0.042990776363386086, allocationInPercentage: 0.042990776363386086,
assetClass: 'EQUITY' as any, assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity', assetClassLabel: 'Equity',
@ -45,6 +46,7 @@ export const holdings: PortfolioPosition[] = [
valueInBaseCurrency: 12230 valueInBaseCurrency: 12230
}, },
{ {
activitiesCount: 2,
allocationInPercentage: 0.02377401948293552, allocationInPercentage: 0.02377401948293552,
assetClass: 'EQUITY' as any, assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity', assetClassLabel: 'Equity',
@ -88,6 +90,7 @@ export const holdings: PortfolioPosition[] = [
valueInBaseCurrency: 6763.224181360202 valueInBaseCurrency: 6763.224181360202
}, },
{ {
activitiesCount: 1,
allocationInPercentage: 0.08038536990007467, allocationInPercentage: 0.08038536990007467,
assetClass: 'EQUITY' as any, assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity', assetClassLabel: 'Equity',
@ -131,6 +134,7 @@ export const holdings: PortfolioPosition[] = [
valueInBaseCurrency: 22868 valueInBaseCurrency: 22868
}, },
{ {
activitiesCount: 1,
allocationInPercentage: 0.19216416482928922, allocationInPercentage: 0.19216416482928922,
assetClass: 'LIQUIDITY' as any, assetClass: 'LIQUIDITY' as any,
assetClassLabel: 'Liquidity', assetClassLabel: 'Liquidity',
@ -162,6 +166,7 @@ export const holdings: PortfolioPosition[] = [
valueInBaseCurrency: 54666.7898248 valueInBaseCurrency: 54666.7898248
}, },
{ {
activitiesCount: 1,
allocationInPercentage: 0.04307127421937313, allocationInPercentage: 0.04307127421937313,
assetClass: 'EQUITY' as any, assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity', assetClassLabel: 'Equity',
@ -205,6 +210,7 @@ export const holdings: PortfolioPosition[] = [
valueInBaseCurrency: 12252.9 valueInBaseCurrency: 12252.9
}, },
{ {
activitiesCount: 1,
allocationInPercentage: 0.18762679306394897, allocationInPercentage: 0.18762679306394897,
assetClass: 'EQUITY' as any, assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity', assetClassLabel: 'Equity',
@ -248,6 +254,7 @@ export const holdings: PortfolioPosition[] = [
valueInBaseCurrency: 53376 valueInBaseCurrency: 53376
}, },
{ {
activitiesCount: 5,
allocationInPercentage: 0.053051250766657634, allocationInPercentage: 0.053051250766657634,
assetClass: 'EQUITY' as any, assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity', assetClassLabel: 'Equity',

19
libs/ui/src/lib/services/data.service.ts

@ -424,22 +424,9 @@ export class DataService {
dataSource: DataSource; dataSource: DataSource;
symbol: string; symbol: string;
}) { }) {
return this.http return this.http.get<PortfolioHoldingResponse>(
.get<PortfolioHoldingResponse>( `/api/v1/portfolio/holding/${dataSource}/${symbol}`
`/api/v1/portfolio/holding/${dataSource}/${symbol}` );
)
.pipe(
map((data) => {
if (data.activities) {
for (const order of data.activities) {
order.createdAt = parseISO(order.createdAt as unknown as string);
order.date = parseISO(order.date as unknown as string);
}
}
return data;
})
);
} }
public fetchInfo(): InfoItem { public fetchInfo(): InfoItem {

20
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.228.0", "version": "2.233.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.228.0", "version": "2.233.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -57,7 +57,7 @@
"class-validator": "0.14.3", "class-validator": "0.14.3",
"color": "5.0.3", "color": "5.0.3",
"countries-and-timezones": "3.8.0", "countries-and-timezones": "3.8.0",
"countries-list": "3.2.0", "countries-list": "3.2.2",
"countup.js": "2.9.0", "countup.js": "2.9.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"dotenv": "17.2.3", "dotenv": "17.2.3",
@ -142,7 +142,7 @@
"jest-environment-jsdom": "30.2.0", "jest-environment-jsdom": "30.2.0",
"jest-preset-angular": "16.0.0", "jest-preset-angular": "16.0.0",
"nx": "22.3.3", "nx": "22.3.3",
"prettier": "3.7.4", "prettier": "3.8.0",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.2.0", "prisma": "7.2.0",
"react": "18.2.0", "react": "18.2.0",
@ -16548,9 +16548,9 @@
} }
}, },
"node_modules/countries-list": { "node_modules/countries-list": {
"version": "3.2.0", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.2.0.tgz", "resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.2.2.tgz",
"integrity": "sha512-HYHAo2fwEsG3TmbsNdVmIQPHizRlqeYMTtLEAl0IANG/3jRYX7p3NR6VapDqKP0n60TmsRy1dyRjVN5JbywDbA==", "integrity": "sha512-ABJ/RWQBrPWy+hRuZoW+0ooK8p65Eo3WmUZwHm6v4wmfSPznNAKzjy3+UUYrJK2v3182BVsgWxdB6ROidj39kw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/countup.js": { "node_modules/countup.js": {
@ -28750,9 +28750,9 @@
} }
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "3.7.4", "version": "3.8.0",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.0.tgz",
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {

6
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.228.0", "version": "2.233.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",
@ -101,7 +101,7 @@
"class-validator": "0.14.3", "class-validator": "0.14.3",
"color": "5.0.3", "color": "5.0.3",
"countries-and-timezones": "3.8.0", "countries-and-timezones": "3.8.0",
"countries-list": "3.2.0", "countries-list": "3.2.2",
"countup.js": "2.9.0", "countup.js": "2.9.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"dotenv": "17.2.3", "dotenv": "17.2.3",
@ -186,7 +186,7 @@
"jest-environment-jsdom": "30.2.0", "jest-environment-jsdom": "30.2.0",
"jest-preset-angular": "16.0.0", "jest-preset-angular": "16.0.0",
"nx": "22.3.3", "nx": "22.3.3",
"prettier": "3.7.4", "prettier": "3.8.0",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.2.0", "prisma": "7.2.0",
"react": "18.2.0", "react": "18.2.0",

Loading…
Cancel
Save