Browse Source

Merge branch 'feature/migrate-from-angular-material-design-2-to-3' into feature/restore-badge-style-filter-indicator

pull/6182/head
Thomas Kaul 3 weeks ago
committed by GitHub
parent
commit
e8555a5821
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 43
      CHANGELOG.md
  2. 41
      README.md
  3. 4
      apps/api/src/app/endpoints/platforms/platforms.controller.ts
  4. 6
      apps/api/src/app/import/import.service.ts
  5. 7
      apps/api/src/app/info/info.service.ts
  6. 49
      apps/api/src/app/order/order.service.ts
  7. 52
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  8. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts
  9. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts
  10. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts
  11. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts
  12. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts
  13. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts
  14. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  15. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts
  16. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts
  17. 148
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts
  18. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts
  19. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts
  20. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts
  21. 7
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts
  22. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
  23. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts
  24. 1
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts
  25. 3
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts
  26. 32
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  27. 1
      apps/api/src/app/portfolio/interfaces/portfolio-order-item.interface.ts
  28. 1
      apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts
  29. 1
      apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts
  30. 10
      apps/api/src/app/portfolio/portfolio.service.ts
  31. 6
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts
  32. 3
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts
  33. 15
      apps/client/project.json
  34. 15
      apps/client/src/app/components/footer/footer.component.html
  35. 14
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  36. 4
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  37. 1
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  38. 13
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  39. 5
      apps/client/src/app/pages/features/features-page.html
  40. 11
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  41. 64
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  42. 1
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  43. 5
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
  44. 330
      apps/client/src/locales/messages.ca.xlf
  45. 330
      apps/client/src/locales/messages.de.xlf
  46. 330
      apps/client/src/locales/messages.es.xlf
  47. 330
      apps/client/src/locales/messages.fr.xlf
  48. 330
      apps/client/src/locales/messages.it.xlf
  49. 8737
      apps/client/src/locales/messages.ko.xlf
  50. 330
      apps/client/src/locales/messages.nl.xlf
  51. 330
      apps/client/src/locales/messages.pl.xlf
  52. 330
      apps/client/src/locales/messages.pt.xlf
  53. 330
      apps/client/src/locales/messages.tr.xlf
  54. 330
      apps/client/src/locales/messages.uk.xlf
  55. 327
      apps/client/src/locales/messages.xlf
  56. 330
      apps/client/src/locales/messages.zh.xlf
  57. 1
      libs/common/src/lib/config.ts
  58. 17
      libs/common/src/lib/helper.ts
  59. 6
      libs/common/src/lib/interfaces/info-item.interface.ts
  60. 8
      libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
  61. 2
      libs/common/src/lib/models/timeline-position.ts
  62. 11
      libs/ui/src/lib/benchmark/benchmark.component.ts
  63. 7
      libs/ui/src/lib/holdings-table/holdings-table.component.html
  64. 4
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  65. 19
      libs/ui/src/lib/services/data.service.ts
  66. 12
      package-lock.json
  67. 4
      package.json

43
CHANGELOG.md

@ -13,6 +13,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Extended the analysis page to include the total amount, change and performance with currency effects
### Changed
- Deprecated `firstBuyDate` in favor of `dateOfFirstActivity` in the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol`
- Improved the language localization for German (`de`)
- Upgraded `countries-list` from version `3.2.0` to `3.2.2`
## 2.231.0 - 2026-01-17
### Changed
- Removed the deprecated platforms from the info service
- Removed the deprecated activities from the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol`
### Fixed
- Fixed a numeric parsing error related to cash positions on the _X-ray_ page
- Fixed the total fee calculation in the holding detail dialog related to activities in a custom currency
- Fixed the total fee calculation in the summary related to activities in a custom currency
## 2.230.0 - 2026-01-14
### Added
- Set up the language localization for Korean (`ko`)
### Changed
- Restored the support for specific calendar year date ranges (`2024`, `2023`, `2022`, etc.) in the holdings table (experimental)
### Fixed
- Fixed the total fee calculation in the holding detail dialog related to activities in a custom currency
- Fixed the total fee calculation in the summary related to activities in a custom currency
## 2.229.0 - 2026-01-11
### Changed ### Changed
- Set the active sort column in the accounts table component - Set the active sort column in the accounts table component
@ -24,8 +64,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed the net worth calculation to prevent the double counting of cash positions
- Fixed the filtering by asset class in the endpoint `GET api/v1/portfolio/holdings` - Fixed the filtering by asset class in the endpoint `GET api/v1/portfolio/holdings`
- Fixed the case-insensitive sorting in the accounts table component - Fixed the case-insensitive sorting in the accounts table component
- Fixed the case-insensitive sorting in the benchmark component
- Fixed the case-insensitive sorting in the holdings table component
## 2.228.0 - 2026-01-03 ## 2.228.0 - 2026-01-03

41
README.md

@ -85,31 +85,32 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
### Supported Environment Variables ### Supported Environment Variables
| Name | Type | Default Value | Description | | Name | Type | Default Value | Description |
| ------------------------ | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | --------------------------- | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens | | `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens |
| `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key | | `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key |
| `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key | | `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key |
| `DATABASE_URL` | `string` | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` | | `DATABASE_URL` | `string` | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` |
| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on | | `ENABLE_FEATURE_AUTH_TOKEN` | `boolean` (optional) | `true` | Enables authentication via security token |
| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) | | `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on |
| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` | | `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) |
| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on | | `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` |
| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database | | `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on |
| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database | | `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database |
| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database | | `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database |
| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ | | `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database |
| `REDIS_HOST` | `string` | | The host where _Redis_ is running | | `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ |
| `REDIS_PASSWORD` | `string` | | The password of _Redis_ | | `REDIS_HOST` | `string` | | The host where _Redis_ is running |
| `REDIS_PORT` | `number` | | The port where _Redis_ is running | | `REDIS_PASSWORD` | `string` | | The password of _Redis_ |
| `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds | | `REDIS_PORT` | `number` | | The port where _Redis_ is running |
| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. | | `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds |
| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. |
#### OpenID Connect OIDC (Experimental) #### OpenID Connect OIDC (Experimental)
| Name | Type | Default Value | Description | | Name | Type | Default Value | Description |
| -------------------------- | --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------- | | -------------------------- | --------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `ENABLE_FEATURE_AUTH_OIDC` | `boolean` (optional) | `false` | Enables _OpenID Connect_ authentication | | `ENABLE_FEATURE_AUTH_OIDC` | `boolean` (optional) | `false` | Enables authentication via _OpenID Connect_ |
| `OIDC_AUTHORIZATION_URL` | `string` (optional) | | Manual override for the OIDC authorization endpoint (falls back to the discovery from the issuer) | | `OIDC_AUTHORIZATION_URL` | `string` (optional) | | Manual override for the OIDC authorization endpoint (falls back to the discovery from the issuer) |
| `OIDC_CALLBACK_URL` | `string` (optional) | `${ROOT_URL}/api/auth/oidc/callback` | The OIDC callback URL | | `OIDC_CALLBACK_URL` | `string` (optional) | `${ROOT_URL}/api/auth/oidc/callback` | The OIDC callback URL |
| `OIDC_CLIENT_ID` | `string` | | The OIDC client ID | | `OIDC_CLIENT_ID` | `string` | | The OIDC client ID |

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

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

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

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

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

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

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

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

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

@ -39,6 +39,7 @@ import { GroupBy } from '@ghostfolio/common/types';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { AssetSubClass } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { plainToClass } from 'class-transformer'; import { plainToClass } from 'class-transformer';
import { import {
@ -119,6 +120,7 @@ export abstract class PortfolioCalculator {
({ ({
date, date,
feeInAssetProfileCurrency, feeInAssetProfileCurrency,
feeInBaseCurrency,
quantity, quantity,
SymbolProfile, SymbolProfile,
tags = [], tags = [],
@ -141,6 +143,7 @@ export abstract class PortfolioCalculator {
type, type,
date: format(date, DATE_FORMAT), date: format(date, DATE_FORMAT),
fee: new Big(feeInAssetProfileCurrency), fee: new Big(feeInAssetProfileCurrency),
feeInBaseCurrency: new Big(feeInBaseCurrency),
quantity: new Big(quantity), quantity: new Big(quantity),
unitPrice: new Big(unitPriceInAssetProfileCurrency) unitPrice: new Big(unitPriceInAssetProfileCurrency)
}; };
@ -335,12 +338,6 @@ export abstract class PortfolioCalculator {
} = {}; } = {};
for (const item of lastTransactionPoint.items) { for (const item of lastTransactionPoint.items) {
const feeInBaseCurrency = item.fee.mul(
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
lastTransactionPoint.date
] ?? 1
);
const marketPriceInBaseCurrency = ( const marketPriceInBaseCurrency = (
marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice marketSymbolMap[endDateString]?.[item.symbol] ?? item.averagePrice
).mul( ).mul(
@ -389,28 +386,34 @@ export abstract class PortfolioCalculator {
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
valuesBySymbol[item.symbol] = { const includeInTotalAssetValue =
currentValues, item.assetSubClass !== AssetSubClass.CASH;
currentValuesWithCurrencyEffect,
investmentValuesAccumulated, if (includeInTotalAssetValue) {
investmentValuesAccumulatedWithCurrencyEffect, valuesBySymbol[item.symbol] = {
investmentValuesWithCurrencyEffect, currentValues,
netPerformanceValues, currentValuesWithCurrencyEffect,
netPerformanceValuesWithCurrencyEffect, investmentValuesAccumulated,
timeWeightedInvestmentValues, investmentValuesAccumulatedWithCurrencyEffect,
timeWeightedInvestmentValuesWithCurrencyEffect investmentValuesWithCurrencyEffect,
}; netPerformanceValues,
netPerformanceValuesWithCurrencyEffect,
timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect
};
}
positions.push({ positions.push({
feeInBaseCurrency, includeInTotalAssetValue,
timeWeightedInvestment, timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect, timeWeightedInvestmentWithCurrencyEffect,
dividend: totalDividend,
dividendInBaseCurrency: totalDividendInBaseCurrency,
averagePrice: item.averagePrice, averagePrice: item.averagePrice,
currency: item.currency, currency: item.currency,
dataSource: item.dataSource, dataSource: item.dataSource,
dividend: totalDividend,
dividendInBaseCurrency: totalDividendInBaseCurrency,
fee: item.fee, fee: item.fee,
feeInBaseCurrency: item.feeInBaseCurrency,
firstBuyDate: item.firstBuyDate, firstBuyDate: item.firstBuyDate,
grossPerformance: !hasErrors ? (grossPerformance ?? null) : null, grossPerformance: !hasErrors ? (grossPerformance ?? null) : null,
grossPerformancePercentage: !hasErrors grossPerformancePercentage: !hasErrors
@ -426,9 +429,8 @@ export abstract class PortfolioCalculator {
investment: totalInvestment, investment: totalInvestment,
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
marketPrice: marketPrice:
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null, marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? 1,
marketPriceInBaseCurrency: marketPriceInBaseCurrency: marketPriceInBaseCurrency?.toNumber() ?? 1,
marketPriceInBaseCurrency?.toNumber() ?? null,
netPerformance: !hasErrors ? (netPerformance ?? null) : null, netPerformance: !hasErrors ? (netPerformance ?? null) : null,
netPerformancePercentage: !hasErrors netPerformancePercentage: !hasErrors
? (netPerformancePercentage ?? null) ? (netPerformancePercentage ?? null)
@ -931,6 +933,7 @@ export abstract class PortfolioCalculator {
for (const { for (const {
date, date,
fee, fee,
feeInBaseCurrency,
quantity, quantity,
SymbolProfile, SymbolProfile,
tags, tags,
@ -995,6 +998,8 @@ export abstract class PortfolioCalculator {
: investment.div(newQuantity).abs(), : investment.div(newQuantity).abs(),
dividend: new Big(0), dividend: new Big(0),
fee: oldAccumulatedSymbol.fee.plus(fee), fee: oldAccumulatedSymbol.fee.plus(fee),
feeInBaseCurrency:
oldAccumulatedSymbol.feeInBaseCurrency.plus(feeInBaseCurrency),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate, firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
includeInHoldings: oldAccumulatedSymbol.includeInHoldings, includeInHoldings: oldAccumulatedSymbol.includeInHoldings,
quantity: newQuantity, quantity: newQuantity,
@ -1007,6 +1012,7 @@ export abstract class PortfolioCalculator {
currency, currency,
dataSource, dataSource,
fee, fee,
feeInBaseCurrency,
skipErrors, skipErrors,
symbol, symbol,
tags, tags,

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

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-22'), date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55, feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -102,6 +103,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65, feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,

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

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-22'), date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55, feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -102,6 +103,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65, feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -117,6 +119,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,

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

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-22'), date: new Date('2021-11-22'),
feeInAssetProfileCurrency: 1.55, feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -102,6 +103,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.65, feeInAssetProfileCurrency: 1.65,
feeInBaseCurrency: 1.65,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,

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

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55, feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -208,6 +209,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55, feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -247,6 +249,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-30'), date: new Date('2021-11-30'),
feeInAssetProfileCurrency: 1.55, feeInAssetProfileCurrency: 1.55,
feeInBaseCurrency: 1.55,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,

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

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

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

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

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

@ -100,6 +100,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2015-01-01'), date: new Date('2015-01-01'),
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 2, quantity: 2,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -115,6 +116,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2017-12-31'), date: new Date('2017-12-31'),
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,

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

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

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

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

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

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

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

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

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

@ -99,6 +99,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2023-01-03'), date: new Date('2023-01-03'),
feeInAssetProfileCurrency: 1, feeInAssetProfileCurrency: 1,
feeInBaseCurrency: 0.9238,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,

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

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

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

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

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

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-09-16'), date: new Date('2021-09-16'),
feeInAssetProfileCurrency: 19, feeInAssetProfileCurrency: 19,
feeInBaseCurrency: 19,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -102,6 +103,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2021-11-16'), date: new Date('2021-11-16'),
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,

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

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

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

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

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

@ -87,6 +87,7 @@ describe('PortfolioCalculator', () => {
...activityDummyData, ...activityDummyData,
date: new Date('2022-01-01'), date: new Date('2022-01-01'),
feeInAssetProfileCurrency: 0, feeInAssetProfileCurrency: 0,
feeInBaseCurrency: 0,
quantity: 1, quantity: 1,
SymbolProfile: { SymbolProfile: {
...symbolProfileDummyData, ...symbolProfileDummyData,
@ -129,7 +130,7 @@ describe('PortfolioCalculator', () => {
grossPerformanceWithCurrencyEffect: new Big('0'), grossPerformanceWithCurrencyEffect: new Big('0'),
investment: new Big('500000'), investment: new Big('500000'),
investmentWithCurrencyEffect: new Big('500000'), investmentWithCurrencyEffect: new Big('500000'),
marketPrice: null, marketPrice: 1,
marketPriceInBaseCurrency: 500000, marketPriceInBaseCurrency: 500000,
netPerformance: new Big('0'), netPerformance: new Big('0'),
netPerformancePercentage: new Big('0'), netPerformancePercentage: new Big('0'),

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

@ -13,7 +13,14 @@ import { PerformanceCalculationType } from '@ghostfolio/common/types/performance
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { addMilliseconds, differenceInDays, format, isBefore } from 'date-fns'; import {
addMilliseconds,
differenceInDays,
eachYearOfInterval,
format,
isBefore,
isThisYear
} from 'date-fns';
import { cloneDeep, sortBy } from 'lodash'; import { cloneDeep, sortBy } from 'lodash';
export class RoaiPortfolioCalculator extends PortfolioCalculator { export class RoaiPortfolioCalculator extends PortfolioCalculator {
@ -34,7 +41,11 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let totalTimeWeightedInvestment = new Big(0); let totalTimeWeightedInvestment = new Big(0);
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0); let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
for (const currentPosition of positions) { for (const currentPosition of positions.filter(
({ includeInTotalAssetValue }) => {
return includeInTotalAssetValue;
}
)) {
if (currentPosition.feeInBaseCurrency) { if (currentPosition.feeInBaseCurrency) {
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
currentPosition.feeInBaseCurrency currentPosition.feeInBaseCurrency
@ -833,15 +844,14 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
'max', 'max',
'mtd', 'mtd',
'wtd', 'wtd',
'ytd' 'ytd',
// TODO: ...eachYearOfInterval({ end, start })
// ...eachYearOfInterval({ end, start }) .filter((date) => {
// .filter((date) => { return !isThisYear(date);
// return !isThisYear(date); })
// }) .map((date) => {
// .map((date) => { return format(date, 'yyyy');
// return format(date, 'yyyy'); })
// })
] as DateRange[]) { ] as DateRange[]) {
const dateInterval = getIntervalFromDateRange(dateRange); const dateInterval = getIntervalFromDateRange(dateRange);
const endDate = dateInterval.endDate; const endDate = dateInterval.endDate;

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

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

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

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

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

@ -8,6 +8,7 @@ export interface TransactionPointSymbol {
dataSource: DataSource; dataSource: DataSource;
dividend: Big; dividend: Big;
fee: Big; fee: Big;
feeInBaseCurrency: Big;
firstBuyDate: string; firstBuyDate: string;
includeInHoldings: boolean; includeInHoldings: boolean;
investment: Big; investment: Big;

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

@ -792,7 +792,7 @@ export class PortfolioService {
averagePrice, averagePrice,
currency, currency,
dividendInBaseCurrency, dividendInBaseCurrency,
fee, feeInBaseCurrency,
firstBuyDate, firstBuyDate,
grossPerformance, grossPerformance,
grossPerformancePercentage, grossPerformancePercentage,
@ -920,19 +920,15 @@ export class PortfolioService {
marketPriceMin, marketPriceMin,
SymbolProfile, SymbolProfile,
tags, tags,
activities: activitiesOfHolding,
activitiesCount: transactionCount, activitiesCount: transactionCount,
averagePrice: averagePrice.toNumber(), averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
dateOfFirstActivity: firstBuyDate,
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
dividendYieldPercent: dividendYieldPercent.toNumber(), dividendYieldPercent: dividendYieldPercent.toNumber(),
dividendYieldPercentWithCurrencyEffect: dividendYieldPercentWithCurrencyEffect:
dividendYieldPercentWithCurrencyEffect.toNumber(), dividendYieldPercentWithCurrencyEffect.toNumber(),
feeInBaseCurrency: this.exchangeRateDataService.toCurrency( feeInBaseCurrency: feeInBaseCurrency.toNumber(),
fee.toNumber(),
SymbolProfile.currency,
userCurrency
),
grossPerformance: grossPerformance?.toNumber(), grossPerformance: grossPerformance?.toNumber(),
grossPerformancePercent: grossPerformancePercentage?.toNumber(), grossPerformancePercent: grossPerformancePercentage?.toNumber(),
grossPerformancePercentWithCurrencyEffect: grossPerformancePercentWithCurrencyEffect:

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

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

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

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

15
apps/client/project.json

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

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

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

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

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

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

@ -215,7 +215,7 @@
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[isDate]="true" [isDate]="true"
[locale]="data.locale" [locale]="data.locale"
[value]="firstBuyDate" [value]="dateOfFirstActivity"
>First Activity</gf-value >First Activity</gf-value
> >
</div> </div>
@ -400,7 +400,7 @@
<gf-historical-market-data-editor <gf-historical-market-data-editor
[currency]="SymbolProfile?.currency" [currency]="SymbolProfile?.currency"
[dataSource]="SymbolProfile?.dataSource" [dataSource]="SymbolProfile?.dataSource"
[dateOfFirstActivity]="firstBuyDate" [dateOfFirstActivity]="dateOfFirstActivity"
[locale]="data.locale" [locale]="data.locale"
[marketData]="marketDataItems" [marketData]="marketDataItems"
[symbol]="SymbolProfile?.symbol" [symbol]="SymbolProfile?.symbol"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -11,7 +11,20 @@ import {
parseISO, parseISO,
subDays subDays
} from 'date-fns'; } from 'date-fns';
import { ca, de, es, fr, it, nl, pl, pt, tr, uk, zhCN } from 'date-fns/locale'; import {
ca,
de,
es,
fr,
it,
ko,
nl,
pl,
pt,
tr,
uk,
zhCN
} from 'date-fns/locale';
import { get, isNil, isString } from 'lodash'; import { get, isNil, isString } from 'lodash';
import { import {
@ -185,6 +198,8 @@ export function getDateFnsLocale(aLanguageCode: string) {
return fr; return fr;
} else if (aLanguageCode === 'it') { } else if (aLanguageCode === 'it') {
return it; return it;
} else if (aLanguageCode === 'ko') {
return ko;
} else if (aLanguageCode === 'nl') { } else if (aLanguageCode === 'nl') {
return nl; return nl;
} else if (aLanguageCode === 'pl') { } else if (aLanguageCode === 'pl') {

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

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

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

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

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

@ -50,6 +50,8 @@ export class TimelinePosition {
@Type(() => Big) @Type(() => Big)
grossPerformanceWithCurrencyEffect: Big; grossPerformanceWithCurrencyEffect: Big;
includeInTotalAssetValue?: boolean;
@Transform(transformToBig, { toClassOnly: true }) @Transform(transformToBig, { toClassOnly: true })
@Type(() => Big) @Type(() => Big)
investment: Big; investment: Big;

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

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

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

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

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

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

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

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

12
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.228.0", "version": "2.231.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.228.0", "version": "2.231.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -56,7 +56,7 @@
"class-validator": "0.14.3", "class-validator": "0.14.3",
"color": "5.0.3", "color": "5.0.3",
"countries-and-timezones": "3.8.0", "countries-and-timezones": "3.8.0",
"countries-list": "3.2.0", "countries-list": "3.2.2",
"countup.js": "2.9.0", "countup.js": "2.9.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"dotenv": "17.2.3", "dotenv": "17.2.3",
@ -16319,9 +16319,9 @@
} }
}, },
"node_modules/countries-list": { "node_modules/countries-list": {
"version": "3.2.0", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.2.0.tgz", "resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.2.2.tgz",
"integrity": "sha512-HYHAo2fwEsG3TmbsNdVmIQPHizRlqeYMTtLEAl0IANG/3jRYX7p3NR6VapDqKP0n60TmsRy1dyRjVN5JbywDbA==", "integrity": "sha512-ABJ/RWQBrPWy+hRuZoW+0ooK8p65Eo3WmUZwHm6v4wmfSPznNAKzjy3+UUYrJK2v3182BVsgWxdB6ROidj39kw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/countup.js": { "node_modules/countup.js": {

4
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.228.0", "version": "2.231.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",
@ -100,7 +100,7 @@
"class-validator": "0.14.3", "class-validator": "0.14.3",
"color": "5.0.3", "color": "5.0.3",
"countries-and-timezones": "3.8.0", "countries-and-timezones": "3.8.0",
"countries-list": "3.2.0", "countries-list": "3.2.2",
"countup.js": "2.9.0", "countup.js": "2.9.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"dotenv": "17.2.3", "dotenv": "17.2.3",

Loading…
Cancel
Save