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": [
"angular.ng-template",
"esbenp.prettier-vscode",
"firsttris.vscode-jest-runner",
"nrwl.angular-console",
"prettier.prettier-vscode"
"nrwl.angular-console"
]
}

2
.vscode/settings.json

@ -1,4 +1,4 @@
{
"editor.defaultFormatter": "prettier.prettier-vscode",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"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/),
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
@ -19,7 +75,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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 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

50
README.md

@ -85,31 +85,32 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
### Supported Environment Variables
| Name | Type | Default Value | Description |
| ------------------------ | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `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_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` |
| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on |
| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) |
| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` |
| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on |
| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database |
| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database |
| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database |
| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ |
| `REDIS_HOST` | `string` | | The host where _Redis_ is running |
| `REDIS_PASSWORD` | `string` | | The password of _Redis_ |
| `REDIS_PORT` | `number` | | The port where _Redis_ is running |
| `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. |
| Name | Type | Default Value | Description |
| --------------------------- | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `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_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` |
| `ENABLE_FEATURE_AUTH_TOKEN` | `boolean` (optional) | `true` | Enables authentication via security token |
| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on |
| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) |
| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` |
| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on |
| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database |
| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database |
| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database |
| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ |
| `REDIS_HOST` | `string` | | The host where _Redis_ is running |
| `REDIS_PASSWORD` | `string` | | The password of _Redis_ |
| `REDIS_PORT` | `number` | | The port where _Redis_ is running |
| `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)
| 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_CALLBACK_URL` | `string` (optional) | `${ROOT_URL}/api/auth/oidc/callback` | The OIDC callback URL |
| `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
<div align="center">
<p>
Browser testing via<br />
<a href="https://www.lambdatest.com?utm_medium=sponsor&utm_source=ghostfolio" target="_blank" title="LambdaTest - AI Powered Testing Tool">
<img alt="LambdaTest Logo" height="45" width="250" src="https://www.lambdatest.com/blue-logo.png" />
</a>
</p>
<a href="https://www.testmu.ai?utm_medium=sponsor&utm_source=ghostfolio" target="_blank" title="TestMu AI - AI Powered Testing Tool">
<img alt="TestMu AI Logo" height="45" src="https://assets.testmu.ai/resources/images/logos/logo.svg" />
</a>
</div>
## Analytics

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

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

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

@ -82,7 +82,7 @@ export class ImportService {
filterBySymbol: symbol
});
const { firstBuyDate, historicalData } = holding;
const { dateOfFirstActivity, historicalData } = holding;
const [{ accounts }, { activities }, [assetProfile], dividends] =
await Promise.all([
@ -95,7 +95,7 @@ export class ImportService {
filters,
userCurrency,
userId,
startDate: parseDate(firstBuyDate)
startDate: parseDate(dateOfFirstActivity)
}),
this.symbolProfileService.getSymbolProfiles([
{
@ -106,7 +106,7 @@ export class ImportService {
await this.dataProviderService.getDividends({
dataSource,
symbol,
from: parseDate(firstBuyDate),
from: parseDate(dateOfFirstActivity),
granularity: 'day',
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 { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
@ -38,7 +37,6 @@ export class InfoService {
private readonly configurationService: ConfigurationService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly jwtService: JwtService,
private readonly platformService: PlatformService,
private readonly propertyService: PropertyService,
private readonly redisCacheService: RedisCacheService,
private readonly subscriptionService: SubscriptionService,
@ -103,16 +101,12 @@ export class InfoService {
benchmarks,
demoAuthToken,
isUserSignupEnabled,
platforms,
statistics,
subscriptionOffer
] = await Promise.all([
this.benchmarkService.getBenchmarkAssetProfiles(),
this.getDemoAuthToken(),
this.propertyService.isUserSignupEnabled(),
this.platformService.getPlatforms({
orderBy: { name: 'asc' }
}),
this.getStatistics(),
this.subscriptionService.getSubscriptionOffer({ key: 'default' })
]);
@ -127,7 +121,6 @@ export class InfoService {
demoAuthToken,
globalPermissions,
isReadOnlyMode,
platforms,
statistics,
subscriptionOffer,
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
* and 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.
* and optional synthetic orders representing cash activities.
*/
@LogPerformance
public async getOrdersForPortfolioCalculator({
filters,
userCurrency,
userId
userId,
withCash = false
}: {
/** Optional filters to apply to the orders. */
filters?: Filter[];
/** The base currency of the user. */
userCurrency: string;
/** The ID of the user. */
userId: string;
/** Whether to include cash activities in the result. */
withCash?: boolean;
}) {
const nonCashOrders = await this.getOrders({
const orders = await this.getOrders({
filters,
userCurrency,
userId,
withExcludedAccountsAndActivities: false // TODO
});
const cashDetails = await this.accountService.getCashDetails({
filters,
userId,
currency: userCurrency
});
if (withCash) {
const cashDetails = await this.accountService.getCashDetails({
filters,
userId,
currency: userCurrency
});
const cashOrders = await this.getCashOrders({
cashDetails,
filters,
userCurrency,
userId
});
const cashOrders = await this.getCashOrders({
cashDetails,
filters,
userCurrency,
userId
});
return {
activities: [...nonCashOrders.activities, ...cashOrders.activities],
count: nonCashOrders.count + cashOrders.count
};
orders.activities.push(...cashOrders.activities);
orders.count += cashOrders.count;
}
return orders;
}
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 { Logger } from '@nestjs/common';
import { AssetSubClass } from '@prisma/client';
import { Big } from 'big.js';
import { plainToClass } from 'class-transformer';
import {
@ -119,6 +120,7 @@ export abstract class PortfolioCalculator {
({
date,
feeInAssetProfileCurrency,
feeInBaseCurrency,
quantity,
SymbolProfile,
tags = [],
@ -141,6 +143,7 @@ export abstract class PortfolioCalculator {
type,
date: format(date, DATE_FORMAT),
fee: new Big(feeInAssetProfileCurrency),
feeInBaseCurrency: new Big(feeInBaseCurrency),
quantity: new Big(quantity),
unitPrice: new Big(unitPriceInAssetProfileCurrency)
};
@ -335,12 +338,6 @@ export abstract class PortfolioCalculator {
} = {};
for (const item of lastTransactionPoint.items) {
const feeInBaseCurrency = item.fee.mul(
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
lastTransactionPoint.date
] ?? 1
);
const marketPriceInBaseCurrency = (
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice
).mul(
@ -389,28 +386,36 @@ export abstract class PortfolioCalculator {
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
valuesBySymbol[item.symbol] = {
currentValues,
currentValuesWithCurrencyEffect,
investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect,
netPerformanceValues,
netPerformanceValuesWithCurrencyEffect,
timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect
};
const includeInTotalAssetValue =
item.assetSubClass !== AssetSubClass.CASH;
if (includeInTotalAssetValue) {
valuesBySymbol[item.symbol] = {
currentValues,
currentValuesWithCurrencyEffect,
investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect,
netPerformanceValues,
netPerformanceValuesWithCurrencyEffect,
timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect
};
}
positions.push({
feeInBaseCurrency,
includeInTotalAssetValue,
timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect,
dividend: totalDividend,
dividendInBaseCurrency: totalDividendInBaseCurrency,
activitiesCount: item.activitiesCount,
averagePrice: item.averagePrice,
currency: item.currency,
dataSource: item.dataSource,
dateOfFirstActivity: item.dateOfFirstActivity,
dividend: totalDividend,
dividendInBaseCurrency: totalDividendInBaseCurrency,
fee: item.fee,
feeInBaseCurrency: item.feeInBaseCurrency,
firstBuyDate: item.firstBuyDate,
grossPerformance: !hasErrors ? (grossPerformance ?? null) : null,
grossPerformancePercentage: !hasErrors
@ -426,9 +431,8 @@ export abstract class PortfolioCalculator {
investment: totalInvestment,
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
marketPrice:
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null,
marketPriceInBaseCurrency:
marketPriceInBaseCurrency?.toNumber() ?? null,
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? 1,
marketPriceInBaseCurrency: marketPriceInBaseCurrency?.toNumber() ?? 1,
netPerformance: !hasErrors ? (netPerformance ?? null) : null,
netPerformancePercentage: !hasErrors
? (netPerformancePercentage ?? null)
@ -931,6 +935,7 @@ export abstract class PortfolioCalculator {
for (const {
date,
fee,
feeInBaseCurrency,
quantity,
SymbolProfile,
tags,
@ -990,11 +995,15 @@ export abstract class PortfolioCalculator {
investment,
skipErrors,
symbol,
activitiesCount: oldAccumulatedSymbol.activitiesCount + 1,
averagePrice: newQuantity.eq(0)
? new Big(0)
: investment.div(newQuantity).abs(),
dateOfFirstActivity: oldAccumulatedSymbol.dateOfFirstActivity,
dividend: new Big(0),
fee: oldAccumulatedSymbol.fee.plus(fee),
feeInBaseCurrency:
oldAccumulatedSymbol.feeInBaseCurrency.plus(feeInBaseCurrency),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
includeInHoldings: oldAccumulatedSymbol.includeInHoldings,
quantity: newQuantity,
@ -1007,10 +1016,13 @@ export abstract class PortfolioCalculator {
currency,
dataSource,
fee,
feeInBaseCurrency,
skipErrors,
symbol,
tags,
activitiesCount: 1,
averagePrice: unitPrice,
dateOfFirstActivity: date,
dividend: new Big(0),
firstBuyDate: date,
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,
date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
@ -102,6 +103,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData,
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
@ -131,15 +133,22 @@ describe('PortfolioCalculator', () => {
groupBy: 'month'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('595.6'),
errors: [],
hasErrors: false,
positions: [
{
activitiesCount: 2,
averagePrice: new Big('139.75'),
currency: 'CHF',
dataSource: 'YAHOO',
dateOfFirstActivity: '2021-11-22',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'),
@ -200,6 +209,10 @@ describe('PortfolioCalculator', () => {
{ date: '2021-11-01', investment: 559 },
{ 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,
date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
@ -102,6 +103,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData,
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
@ -117,6 +119,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData,
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
@ -146,15 +149,22 @@ describe('PortfolioCalculator', () => {
groupBy: 'month'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'),
errors: [],
hasErrors: false,
positions: [
{
activitiesCount: 3,
averagePrice: new Big('0'),
currency: 'CHF',
dataSource: 'YAHOO',
dateOfFirstActivity: '2021-11-22',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'),
@ -213,6 +223,10 @@ describe('PortfolioCalculator', () => {
{ date: '2021-11-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,
date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
@ -102,6 +103,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData,
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
@ -131,15 +133,22 @@ describe('PortfolioCalculator', () => {
groupBy: 'month'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('0'),
errors: [],
hasErrors: false,
positions: [
{
activitiesCount: 2,
averagePrice: new Big('0'),
currency: 'CHF',
dataSource: 'YAHOO',
dateOfFirstActivity: '2021-11-22',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('3.2'),
@ -198,6 +207,10 @@ describe('PortfolioCalculator', () => {
{ date: '2021-11-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,
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
@ -122,15 +123,22 @@ describe('PortfolioCalculator', () => {
groupBy: 'month'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('297.8'),
errors: [],
hasErrors: false,
positions: [
{
activitiesCount: 1,
averagePrice: new Big('136.6'),
currency: 'CHF',
dataSource: 'YAHOO',
dateOfFirstActivity: '2021-11-30',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('1.55'),
@ -198,6 +206,10 @@ describe('PortfolioCalculator', () => {
{ date: '2021-11-01', investment: 273.2 },
{ 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 () => {
@ -208,6 +220,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData,
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
@ -247,6 +260,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData,
date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2,
SymbolProfile: {
...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,
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 3.94,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',

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

@ -98,6 +98,7 @@ describe('PortfolioCalculator', () => {
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 4.46,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
@ -131,6 +132,11 @@ describe('PortfolioCalculator', () => {
groupBy: 'month'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot.historicalData[0]).toEqual({
date: '2021-12-11',
investmentValueWithCurrencyEffect: 0,
@ -189,9 +195,11 @@ describe('PortfolioCalculator', () => {
hasErrors: false,
positions: [
{
activitiesCount: 1,
averagePrice: new Big('44558.42'),
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: '2021-12-12',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('4.46'),
@ -244,6 +252,11 @@ describe('PortfolioCalculator', () => {
{ date: '2021-12-01', investment: 44558.42 },
{ 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,
date: new Date('2015-01-01'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 2,
SymbolProfile: {
...symbolProfileDummyData,
@ -115,6 +116,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData,
date: new Date('2017-12-31'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
@ -144,6 +146,11 @@ describe('PortfolioCalculator', () => {
groupBy: 'month'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('13298.425356'),
errors: [],
@ -151,9 +158,11 @@ describe('PortfolioCalculator', () => {
hasErrors: false,
positions: [
{
activitiesCount: 2,
averagePrice: new Big('320.43'),
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: '2015-01-01',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('0'),
@ -251,6 +260,13 @@ describe('PortfolioCalculator', () => {
{ date: '2017-12-01', investment: -318.54266729999995 },
{ 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,
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
feeInBaseCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',

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

@ -98,6 +98,7 @@ describe('PortfolioCalculator', () => {
...activity,
date: parseDate(activity.date),
feeInAssetProfileCurrency: 4.46,
feeInBaseCurrency: 4.46,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
@ -131,6 +132,11 @@ describe('PortfolioCalculator', () => {
groupBy: 'month'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot.historicalData[0]).toEqual({
date: '2021-12-11',
investmentValueWithCurrencyEffect: 0,
@ -189,9 +195,11 @@ describe('PortfolioCalculator', () => {
hasErrors: false,
positions: [
{
activitiesCount: 1,
averagePrice: new Big('44558.42'),
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: '2021-12-12',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('4.46'),
@ -244,6 +252,11 @@ describe('PortfolioCalculator', () => {
{ date: '2021-12-01', investment: 44558.42 },
{ 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 { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
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 { DataSource } from '@prisma/client';
@ -191,7 +191,8 @@ describe('PortfolioCalculator', () => {
const { activities } = await orderService.getOrdersForPortfolioCalculator(
{
userCurrency: 'CHF',
userId: userDummyData.id
userId: userDummyData.id,
withCash: true
}
);
@ -201,7 +202,14 @@ describe('PortfolioCalculator', () => {
values: []
});
const accountBalanceItems =
await accountBalanceService.getAccountBalanceItems({
userCurrency: 'CHF',
userId: userDummyData.id
});
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
accountBalanceItems,
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: 'CHF',
@ -210,94 +218,74 @@ describe('PortfolioCalculator', () => {
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const historicalData20231231 = portfolioSnapshot.historicalData.find(
({ date }) => {
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
const position = portfolioSnapshot.positions.find(({ symbol }) => {
return symbol === 'USD';
});
/**
* 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 with currency effect: (1000 USD * 0.9) - (1000 USD * 0.85) = 50 CHF
* Total investment: 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
* Total account balance: 2000 USD * 0.85 = 1700 CHF (using the exchange rate on 2024-12-31)
* Value in base currency: 2000 USD * 0.91 = 1820 CHF
*/
expect(historicalData20241231).toMatchObject<HistoricalDataItem>({
date: '2024-12-31',
investmentValueWithCurrencyEffect: 900,
netPerformance: 0,
netPerformanceInPercentage: 0,
netPerformanceInPercentageWithCurrencyEffect: 0.058823529411764705,
netPerformanceWithCurrencyEffect: 50,
netWorth: 1800,
totalAccountBalance: 0,
totalInvestment: 1820,
totalInvestmentValueWithCurrencyEffect: 1750,
value: 1820,
valueWithCurrencyEffect: 1800
expect(position).toMatchObject<TimelinePosition>({
activitiesCount: 2,
averagePrice: new Big(1),
currency: 'USD',
dataSource: DataSource.YAHOO,
dateOfFirstActivity: '2023-12-31',
dividend: new Big(0),
dividendInBaseCurrency: new Big(0),
fee: new Big(0),
feeInBaseCurrency: new Big(0),
firstBuyDate: '2023-12-31',
grossPerformance: new Big(0),
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({
hasErrors: false,
totalFeesWithCurrencyEffect: new Big('0'),
totalInterestWithCurrencyEffect: new Big('0'),
totalLiabilitiesWithCurrencyEffect: new Big('0')
totalFeesWithCurrencyEffect: new Big(0),
totalInterestWithCurrencyEffect: 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,
date: new Date('2021-09-01'),
feeInAssetProfileCurrency: 49,
feeInBaseCurrency: 49,
quantity: 0,
SymbolProfile: {
...symbolProfileDummyData,

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

@ -99,6 +99,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData,
date: new Date('2023-01-03'),
feeInAssetProfileCurrency: 1,
feeInBaseCurrency: 0.9238,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
@ -128,15 +129,22 @@ describe('PortfolioCalculator', () => {
groupBy: 'month'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('103.10483'),
errors: [],
hasErrors: false,
positions: [
{
activitiesCount: 1,
averagePrice: new Big('89.12'),
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: '2023-01-03',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('1'),
@ -217,6 +225,10 @@ describe('PortfolioCalculator', () => {
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,
date: new Date('2023-01-01'), // Date in future
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,

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

@ -80,6 +80,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData,
date: new Date('2024-03-08'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 0.3333333333333333,
SymbolProfile: {
...symbolProfileDummyData,
@ -94,8 +95,9 @@ describe('PortfolioCalculator', () => {
{
...activityDummyData,
date: new Date('2024-03-13'),
quantity: 0.6666666666666666,
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 0.6666666666666666,
SymbolProfile: {
...symbolProfileDummyData,
currency: 'USD',
@ -109,8 +111,9 @@ describe('PortfolioCalculator', () => {
{
...activityDummyData,
date: new Date('2024-03-14'),
quantity: 1,
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
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,
date: new Date('2021-09-16'),
feeInAssetProfileCurrency: 19,
feeInBaseCurrency: 19,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
@ -102,6 +103,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData,
date: new Date('2021-11-16'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
@ -129,9 +131,11 @@ describe('PortfolioCalculator', () => {
hasErrors: false,
positions: [
{
activitiesCount: 2,
averagePrice: new Big('298.58'),
currency: 'USD',
dataSource: 'YAHOO',
dateOfFirstActivity: '2021-09-16',
dividend: new Big('0.62'),
dividendInBaseCurrency: new Big('0.62'),
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'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big(0),
hasErrors: false,
@ -108,6 +113,8 @@ describe('PortfolioCalculator', () => {
expect(investments).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,
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
feeInBaseCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: activity.currency,
@ -128,15 +129,22 @@ describe('PortfolioCalculator', () => {
groupBy: 'month'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot).toMatchObject({
currentValueInBaseCurrency: new Big('87.8'),
errors: [],
hasErrors: false,
positions: [
{
activitiesCount: 2,
averagePrice: new Big('75.80'),
currency: 'CHF',
dataSource: 'YAHOO',
dateOfFirstActivity: '2022-03-07',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('4.25'),
@ -197,6 +205,10 @@ describe('PortfolioCalculator', () => {
{ date: '2022-03-01', investment: 151.6 },
{ 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,
date: parseDate(activity.date),
feeInAssetProfileCurrency: activity.fee,
feeInBaseCurrency: activity.fee,
SymbolProfile: {
...symbolProfileDummyData,
currency: activity.currency,
@ -128,6 +129,11 @@ describe('PortfolioCalculator', () => {
groupBy: 'month'
});
const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({
data: portfolioSnapshot.historicalData,
groupBy: 'year'
});
expect(portfolioSnapshot.historicalData[0]).toEqual({
date: '2022-03-06',
investmentValueWithCurrencyEffect: 0,
@ -187,9 +193,11 @@ describe('PortfolioCalculator', () => {
hasErrors: false,
positions: [
{
activitiesCount: 2,
averagePrice: new Big('0'),
currency: 'CHF',
dataSource: 'YAHOO',
dateOfFirstActivity: '2022-03-07',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('0'),
@ -248,6 +256,10 @@ describe('PortfolioCalculator', () => {
{ date: '2022-03-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,
date: new Date('2022-01-01'),
feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1,
SymbolProfile: {
...symbolProfileDummyData,
@ -115,9 +116,11 @@ describe('PortfolioCalculator', () => {
hasErrors: false,
positions: [
{
activitiesCount: 1,
averagePrice: new Big('500000'),
currency: 'USD',
dataSource: 'MANUAL',
dateOfFirstActivity: '2022-01-01',
dividend: new Big('0'),
dividendInBaseCurrency: new Big('0'),
fee: new Big('0'),
@ -129,7 +132,7 @@ describe('PortfolioCalculator', () => {
grossPerformanceWithCurrencyEffect: new Big('0'),
investment: new Big('500000'),
investmentWithCurrencyEffect: new Big('500000'),
marketPrice: null,
marketPrice: 1,
marketPriceInBaseCurrency: 500000,
netPerformance: 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 { 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';
export class RoaiPortfolioCalculator extends PortfolioCalculator {
@ -34,7 +41,11 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let totalTimeWeightedInvestment = 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) {
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
currentPosition.feeInBaseCurrency
@ -833,15 +844,14 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
'max',
'mtd',
'wtd',
'ytd'
// TODO:
// ...eachYearOfInterval({ end, start })
// .filter((date) => {
// return !isThisYear(date);
// })
// .map((date) => {
// return format(date, 'yyyy');
// })
'ytd',
...eachYearOfInterval({ end, start })
.filter((date) => {
return !isThisYear(date);
})
.map((date) => {
return format(date, 'yyyy');
})
] as DateRange[]) {
const dateInterval = getIntervalFromDateRange(dateRange);
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';
export interface PortfolioOrderItem extends PortfolioOrder {
feeInBaseCurrency?: Big;
feeInBaseCurrencyWithCurrencyEffect?: Big;
itemType?: 'end' | 'start';
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'> {
date: string;
fee: Big;
feeInBaseCurrency: Big;
quantity: Big;
SymbolProfile: Pick<
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';
export interface TransactionPointSymbol {
activitiesCount: number;
assetSubClass: AssetSubClass;
averagePrice: Big;
currency: string;
dataSource: DataSource;
dateOfFirstActivity: string;
dividend: Big;
fee: Big;
feeInBaseCurrency: Big;
/** @deprecated use dateOfFirstActivity instead */
firstBuyDate: string;
includeInHoldings: boolean;
investment: Big;
quantity: Big;
skipErrors: boolean;
symbol: string;
tags?: Tag[];
/** @deprecated use activitiesCount instead */
transactionCount: number;
}

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

@ -179,9 +179,9 @@ export class PortfolioService {
return Promise.all(
accounts.map(async (account) => {
let activitiesCount = 0;
let dividendInBaseCurrency = 0;
let interestInBaseCurrency = 0;
let transactionCount = 0;
for (const {
currency,
@ -214,7 +214,7 @@ export class PortfolioService {
}
if (!isDraft) {
transactionCount += 1;
activitiesCount += 1;
}
}
@ -223,9 +223,9 @@ export class PortfolioService {
const result = {
...account,
activitiesCount,
dividendInBaseCurrency,
interestInBaseCurrency,
transactionCount,
valueInBaseCurrency,
allocationInPercentage: 0,
balanceInBaseCurrency: this.exchangeRateDataService.toCurrency(
@ -233,6 +233,7 @@ export class PortfolioService {
account.currency,
userCurrency
),
transactionCount: activitiesCount,
value: this.exchangeRateDataService.toCurrency(
valueInBaseCurrency,
userCurrency,
@ -262,6 +263,8 @@ export class PortfolioService {
withExcludedAccounts
});
let activitiesCount = 0;
const searchQuery = filters.find(({ type }) => {
return type === 'SEARCH_QUERY';
})?.id;
@ -284,6 +287,8 @@ export class PortfolioService {
let transactionCount = 0;
for (const account of accounts) {
activitiesCount += account.activitiesCount;
totalBalanceInBaseCurrency = totalBalanceInBaseCurrency.plus(
account.balanceInBaseCurrency
);
@ -296,6 +301,7 @@ export class PortfolioService {
totalValueInBaseCurrency = totalValueInBaseCurrency.plus(
account.valueInBaseCurrency
);
transactionCount += account.transactionCount;
}
@ -310,6 +316,7 @@ export class PortfolioService {
return {
accounts,
activitiesCount,
transactionCount,
totalBalanceInBaseCurrency: totalBalanceInBaseCurrency.toNumber(),
totalDividendInBaseCurrency: totalDividendInBaseCurrency.toNumber(),
@ -567,6 +574,7 @@ export class PortfolioService {
}
for (const {
activitiesCount,
currency,
dividend,
firstBuyDate,
@ -610,6 +618,7 @@ export class PortfolioService {
}
holdings[symbol] = {
activitiesCount,
currency,
markets,
marketsAdvanced,
@ -789,10 +798,11 @@ export class PortfolioService {
}
const {
activitiesCount,
averagePrice,
currency,
dividendInBaseCurrency,
fee,
feeInBaseCurrency,
firstBuyDate,
grossPerformance,
grossPerformancePercentage,
@ -807,8 +817,7 @@ export class PortfolioService {
quantity,
tags,
timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect,
transactionCount
timeWeightedInvestmentWithCurrencyEffect
} = holding;
const activitiesOfHolding = activities.filter(({ SymbolProfile }) => {
@ -914,25 +923,20 @@ export class PortfolioService {
);
return {
firstBuyDate,
activitiesCount,
marketPrice,
marketPriceMax,
marketPriceMin,
SymbolProfile,
tags,
activities: activitiesOfHolding,
activitiesCount: transactionCount,
averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
dateOfFirstActivity: firstBuyDate,
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
dividendYieldPercent: dividendYieldPercent.toNumber(),
dividendYieldPercentWithCurrencyEffect:
dividendYieldPercentWithCurrencyEffect.toNumber(),
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
fee.toNumber(),
SymbolProfile.currency,
userCurrency
),
feeInBaseCurrency: feeInBaseCurrency.toNumber(),
grossPerformance: grossPerformance?.toNumber(),
grossPerformancePercent: grossPerformancePercentage?.toNumber(),
grossPerformancePercentWithCurrencyEffect:
@ -1662,6 +1666,7 @@ export class PortfolioService {
}): PortfolioPosition {
return {
currency,
activitiesCount: 0,
allocationInPercentage: 0,
assetClass: AssetClass.LIQUIDITY,
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 = {
getExchangeRatesByCurrency: ({ targetCurrency }): Promise<any> => {
import { ExchangeRateDataService } from './exchange-rate-data.service';
export const ExchangeRateDataServiceMock: Partial<ExchangeRateDataService> = {
getExchangeRatesByCurrency: ({ targetCurrency }) => {
if (targetCurrency === 'CHF') {
return Promise.resolve({
CHFCHF: {

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

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

15
apps/client/project.json

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

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

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

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

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

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

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

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

@ -24,7 +24,7 @@
}
.mdc-button {
height: 100%;
color: var(--mdc-text-button-label-text-color);
&:not(.mat-primary) {
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 dataSource: MatTableDataSource<Activity>;
public dateOfFirstActivity: string;
public dividendInBaseCurrency: number;
public dividendInBaseCurrencyPrecision = 2;
public dividendYieldPercentWithCurrencyEffect: number;
public feeInBaseCurrency: number;
public firstBuyDate: string;
public hasPermissionToCreateOwnTag: boolean;
public hasPermissionToReadMarketDataOfOwnAssetProfile: boolean;
public historicalDataItems: LineChartItem[];
@ -267,10 +267,10 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
activitiesCount,
averagePrice,
dataProviderInfo,
dateOfFirstActivity,
dividendInBaseCurrency,
dividendYieldPercentWithCurrencyEffect,
feeInBaseCurrency,
firstBuyDate,
historicalData,
investmentInBaseCurrencyWithCurrencyEffect,
marketPrice,
@ -298,6 +298,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
this.benchmarkDataItems = [];
this.countries = {};
this.dataProviderInfo = dataProviderInfo;
this.dateOfFirstActivity = dateOfFirstActivity;
this.dividendInBaseCurrency = dividendInBaseCurrency;
if (
@ -312,7 +313,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
dividendYieldPercentWithCurrencyEffect;
this.feeInBaseCurrency = feeInBaseCurrency;
this.firstBuyDate = firstBuyDate;
this.hasPermissionToReadMarketDataOfOwnAssetProfile =
hasPermission(
@ -461,16 +461,16 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
}
}
if (isToday(parseISO(this.firstBuyDate))) {
if (isToday(parseISO(this.dateOfFirstActivity))) {
// Add average price
this.historicalDataItems.push({
date: this.firstBuyDate,
date: this.dateOfFirstActivity,
value: this.averagePrice
});
// Add benchmark 1
this.benchmarkDataItems.push({
date: this.firstBuyDate,
date: this.dateOfFirstActivity,
value: averagePrice
});
@ -501,7 +501,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
if (
this.benchmarkDataItems[0]?.value === undefined &&
isSameMonth(parseISO(this.firstBuyDate), new Date())
isSameMonth(parseISO(this.dateOfFirstActivity), new Date())
) {
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"
[isDate]="true"
[locale]="data.locale"
[value]="firstBuyDate"
[value]="dateOfFirstActivity"
>First Activity</gf-value
>
</div>
@ -400,7 +400,7 @@
<gf-historical-market-data-editor
[currency]="SymbolProfile?.currency"
[dataSource]="SymbolProfile?.dataSource"
[dateOfFirstActivity]="firstBuyDate"
[dateOfFirstActivity]="dateOfFirstActivity"
[locale]="data.locale"
[marketData]="marketDataItems"
[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',
'fr',
'it',
'ko',
'nl',
'pl',
'pt',

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

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

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

@ -207,18 +207,20 @@
<div class="col-12">
<h2 class="h4 mb-3">Sponsors</h2>
<div class="text-center">
<small>Browser testing via</small>
<br />
<div class="mb-2 small">Browser testing via</div>
<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"
title="LambdaTest - AI Powered Testing Tool"
title="TestMu AI - AI Powered Testing Tool"
>
<img
alt="LambdaTest Logo"
height="45"
src="../assets/images/sponsors/logo-lambdatest.png"
width="250"
alt="TestMu AI Logo"
height="32"
[src]="
user?.settings?.colorScheme === 'LIGHT'
? '../assets/images/sponsors/logo-testmu-dark.svg'
: '../assets/images/sponsors/logo-testmu-light.svg'
"
/>
</a>
</div>

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

@ -260,8 +260,9 @@
<p class="m-0">
Use Ghostfolio in multiple languages: English,
<!-- Català, -->
Chinese, Dutch, French, German, Italian, Polish, Portuguese,
Spanish and Turkish
Chinese, Dutch, French, German, Italian,
<!-- Korean, -->
Polish, Portuguese, Spanish and Turkish
<!-- and Ukrainian -->
are currently supported.
</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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config';
import {
HistoricalDataItem,
InvestmentItem,
@ -94,6 +95,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
public performanceDataItems: HistoricalDataItem[];
public performanceDataItemsInPercentage: HistoricalDataItem[];
public portfolioEvolutionDataLabel = $localize`Investment`;
public precision = 2;
public streaks: PortfolioInvestmentsResponse['streaks'];
public top3: PortfolioPosition[];
public unitCurrentStreak: string;
@ -317,12 +319,21 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
: valueInPercentage
});
}
this.performanceDataItemsInPercentage.push({
date,
value: netPerformanceInPercentageWithCurrencyEffect
});
}
if (
this.deviceType === 'mobile' &&
this.performance.currentValueInBaseCurrency >=
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES
) {
this.precision = 0;
}
this.isLoadingInvestmentChart = false;
this.updateBenchmarkDataItems();

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

@ -74,6 +74,72 @@
</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="col-lg">
<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',
'Interactive Brokers',
'Mintos',
'Monefit SmartSaver',
'Swissquote',
'VIAC',
'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,
key: 'ghostfolio',
languages: [
'Chinese (简体中文)',
'Deutsch',
'English',
'Español',
'Français',
'Italiano',
'Korean (한국어)',
'Nederlands',
'Português',
'Türkçe',
'简体中文'
'Türkçe'
],
name: 'Ghostfolio',
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 = {
lastPublish: '{BUILD_TIMESTAMP}',
production: true,
stripePublicKey: ''
production: true
};

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

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

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/table';
@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 {
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 {
&.mat-accent,
&.mat-primary {
@ -305,10 +271,6 @@ body {
.page {
&.has-tabs {
.mat-mdc-tab-nav-bar {
--mat-tab-inactive-label-text-color: rgba(var(--light-primary-text));
}
@media (min-width: 576px) {
.mat-mdc-tab-header {
background-color: rgba(var(--palette-foreground-base-dark), 0.02);
@ -443,8 +405,6 @@ ngx-skeleton-loader {
.mat-mdc-card {
.mat-mdc-card-title {
--mat-card-title-text-line-height: 1.2;
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 {
&.without-hint {
.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-horizontal {
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 {
padding: 2rem 0;
}
@media (max-width: 575.98px) {
.mat-mdc-tab-link {
--mat-tab-container-height: 3rem;
}
}
@media (min-width: 576px) {
flex-direction: row-reverse;
@include mat.tabs-overrides(
(
divider-height: 0
)
);
.mat-mdc-tab-header {
background-color: rgba(var(--palette-foreground-base), 0.02);
padding: 2rem 0;
width: 14rem;
--mat-tab-label-text-tracking: normal;
--mat-tab-container-height: 2rem;
.mat-mdc-tab-links {
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 'sass:map';
@use './variables.scss' as variables;
$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
)
);
$dark-primary-text: rgba(black, 0.87);
$light-primary-text: white;
$gf-secondary: (
50: var(--gf-theme-secondary-50),
100: var(--gf-theme-secondary-100),
200: var(--gf-theme-secondary-200),
300: var(--gf-theme-secondary-300),
400: var(--gf-theme-secondary-400),
500: var(--gf-theme-secondary-500),
600: var(--gf-theme-secondary-600),
700: var(--gf-theme-secondary-700),
800: var(--gf-theme-secondary-800),
900: var(--gf-theme-secondary-900),
A100: var(--gf-theme-secondary-A100),
A200: var(--gf-theme-secondary-A200),
A400: var(--gf-theme-secondary-A400),
A700: var(--gf-theme-secondary-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
$_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
)
);
$gf-typography: mat.m2-define-typography-config();
// Create default theme
$gf-theme-default: mat.m2-define-light-theme(
(
color: (
accent: mat.m2-define-palette($gf-secondary, 500, 900, A100),
primary: mat.m2-define-palette($gf-primary)
),
density: -3,
typography: $gf-typography
)
$_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);
@include mat.all-component-themes($gf-theme-default);
// Create dark theme
$gf-theme-dark: mat.m2-define-dark-theme(
(
color: (
accent: mat.m2-define-palette($gf-secondary, 500, 900, A100),
primary: mat.m2-define-palette($gf-primary)
),
density: -3,
typography: $gf-typography
)
);
@include mat.app-background();
@include mat.button-density(0);
@include mat.elevation-classes();
@include mat.table-density(-1);
// $gf-typography: mat.m2-define-typography-config();
.theme-light {
$gf-theme-default: mat.define-theme(
(
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 {
@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);
@include mat.elevation-classes();
@include mat.app-background();
@include mat.table-density(-1);
.theme-dark,
.theme-light {
@media (max-width: 575.98px) {
@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 {
--gf-theme-alpha-hover: 0.04;

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

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

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

@ -11,7 +11,21 @@ import {
parseISO,
subDays
} 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 {
DEFAULT_CURRENCY,
@ -184,6 +198,8 @@ export function getDateFnsLocale(aLanguageCode: string) {
return fr;
} else if (aLanguageCode === 'it') {
return it;
} else if (aLanguageCode === 'ko') {
return ko;
} else if (aLanguageCode === 'nl') {
return nl;
} else if (aLanguageCode === 'pl') {
@ -242,6 +258,16 @@ export function getLocale() {
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) {
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 { PortfolioReportRule } from './portfolio-report-rule.interface';
import type { PortfolioSummary } from './portfolio-summary.interface';
import type { Position } from './position.interface';
import type { Product } from './product';
import type { AccessTokenResponse } from './responses/access-token-response.interface';
import type { AccountBalancesResponse } from './responses/account-balances-response.interface';
@ -172,7 +171,6 @@ export {
PortfolioReportResponse,
PortfolioReportRule,
PortfolioSummary,
Position,
Product,
PublicKeyCredentialCreationOptionsJSON,
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 { SubscriptionOffer } from './subscription-offer.interface';
@ -13,10 +13,6 @@ export interface InfoItem {
globalPermissions: string[];
isDataGatheringEnabled?: string;
isReadOnlyMode?: boolean;
/** @deprecated */
platforms: Platform[];
statistics: Statistics;
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';
export interface PortfolioPosition {
activitiesCount: number;
allocationInPercentage: number;
assetClass?: AssetClass;
assetClassLabel?: string;
@ -38,7 +39,10 @@ export interface PortfolioPosition {
sectors: Sector[];
symbol: string;
tags?: Tag[];
/** @deprecated use activitiesCount instead */
transactionCount: number;
type?: string;
url?: string;
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 {
accounts: AccountWithValue[];
activitiesCount: number;
totalBalanceInBaseCurrency: number;
totalDividendInBaseCurrency: number;
totalInterestInBaseCurrency: number;
totalValueInBaseCurrency: number;
/** @deprecated use activitiesCount instead */
transactionCount: number;
}

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

@ -1,5 +1,4 @@
import {
Activity,
Benchmark,
DataProviderInfo,
EnhancedSymbolProfile,
@ -9,17 +8,14 @@ import {
import { Tag } from '@prisma/client';
export interface PortfolioHoldingResponse {
/** @deprecated */
activities: Activity[];
activitiesCount: number;
averagePrice: number;
dataProviderInfo: DataProviderInfo;
dateOfFirstActivity: string;
dividendInBaseCurrency: number;
dividendYieldPercent: number;
dividendYieldPercentWithCurrencyEffect: number;
feeInBaseCurrency: number;
firstBuyDate: string;
grossPerformance: number;
grossPerformancePercent: 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';
export class TimelinePosition {
activitiesCount: number;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
averagePrice: Big;
currency: string;
dataSource: DataSource;
dateOfFirstActivity: string;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
@ -32,6 +35,7 @@ export class TimelinePosition {
@Type(() => Big)
feeInBaseCurrency: Big;
/** @deprecated use dateOfFirstActivity instead */
firstBuyDate: string;
@Transform(transformToBig, { toClassOnly: true })
@ -50,6 +54,8 @@ export class TimelinePosition {
@Type(() => Big)
grossPerformanceWithCurrencyEffect: Big;
includeInTotalAssetValue?: boolean;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
investment: Big;
@ -90,6 +96,7 @@ export class TimelinePosition {
@Type(() => Big)
timeWeightedInvestmentWithCurrencyEffect: Big;
/** @deprecated use activitiesCount instead */
transactionCount: number;
@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';
export type AccountWithValue = AccountModel & {
activitiesCount: number;
allocationInPercentage: number;
balanceInBaseCurrency: number;
dividendInBaseCurrency: number;
interestInBaseCurrency: number;
platform?: Platform;
/** @deprecated use activitiesCount instead */
transactionCount: number;
value: 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 { getLocale } from '@ghostfolio/common/helper';
import { getLocale, getLowercase } from '@ghostfolio/common/helper';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfValueComponent } from '@ghostfolio/ui/value';
@ -32,7 +32,6 @@ import {
trashOutline,
walletOutline
} from 'ionicons/icons';
import { get } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, Subscription } from 'rxjs';
@ -133,8 +132,9 @@ export class GfAccountsTableComponent implements OnChanges, OnDestroy {
this.isLoading = true;
this.dataSource = new MatTableDataSource(this.accounts);
this.dataSource.sortingDataAccessor = getLowercase;
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = get;
if (this.accounts) {
this.isLoading = false;

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

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

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

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

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

@ -19,12 +19,7 @@
</ng-container>
<ng-container matColumnDef="nameWithSymbol">
<th
*matHeaderCellDef
class="px-1"
mat-header-cell
mat-sort-header="symbol"
>
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header="name">
<ng-container i18n>Name</ng-container>
</th>
<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 {
AssetProfileIdentifier,
PortfolioPosition
@ -92,6 +92,8 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy {
this.dataSource = new MatTableDataSource(this.holdings);
this.dataSource.paginator = this.paginator;
this.dataSource.sortingDataAccessor = getLowercase;
this.dataSource.sort = this.sort;
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[] = [
{
activitiesCount: 1,
allocationInPercentage: 0.042990776363386086,
assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity',
@ -45,6 +46,7 @@ export const holdings: PortfolioPosition[] = [
valueInBaseCurrency: 12230
},
{
activitiesCount: 2,
allocationInPercentage: 0.02377401948293552,
assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity',
@ -88,6 +90,7 @@ export const holdings: PortfolioPosition[] = [
valueInBaseCurrency: 6763.224181360202
},
{
activitiesCount: 1,
allocationInPercentage: 0.08038536990007467,
assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity',
@ -131,6 +134,7 @@ export const holdings: PortfolioPosition[] = [
valueInBaseCurrency: 22868
},
{
activitiesCount: 1,
allocationInPercentage: 0.19216416482928922,
assetClass: 'LIQUIDITY' as any,
assetClassLabel: 'Liquidity',
@ -162,6 +166,7 @@ export const holdings: PortfolioPosition[] = [
valueInBaseCurrency: 54666.7898248
},
{
activitiesCount: 1,
allocationInPercentage: 0.04307127421937313,
assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity',
@ -205,6 +210,7 @@ export const holdings: PortfolioPosition[] = [
valueInBaseCurrency: 12252.9
},
{
activitiesCount: 1,
allocationInPercentage: 0.18762679306394897,
assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity',
@ -248,6 +254,7 @@ export const holdings: PortfolioPosition[] = [
valueInBaseCurrency: 53376
},
{
activitiesCount: 5,
allocationInPercentage: 0.053051250766657634,
assetClass: 'EQUITY' as any,
assetClassLabel: 'Equity',

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

@ -424,22 +424,9 @@ export class DataService {
dataSource: DataSource;
symbol: string;
}) {
return this.http
.get<PortfolioHoldingResponse>(
`/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;
})
);
return this.http.get<PortfolioHoldingResponse>(
`/api/v1/portfolio/holding/${dataSource}/${symbol}`
);
}
public fetchInfo(): InfoItem {

20
package-lock.json

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

6
package.json

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

Loading…
Cancel
Save