Browse Source

Merge branch 'main' into feature/migrate-from-angular-material-design-2-to-3

pull/6182/head^2
Thomas Kaul 1 week ago
parent
commit
4b5eba327b
  1. 46
      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. 152
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  32. 6
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts
  33. 3
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts
  34. 15
      apps/client/project.json
  35. 15
      apps/client/src/app/components/footer/footer.component.html
  36. 14
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  37. 4
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  38. 1
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  39. 13
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  40. 5
      apps/client/src/app/pages/features/features-page.html
  41. 11
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  42. 64
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  43. 1
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  44. 5
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
  45. 330
      apps/client/src/locales/messages.ca.xlf
  46. 330
      apps/client/src/locales/messages.de.xlf
  47. 330
      apps/client/src/locales/messages.es.xlf
  48. 330
      apps/client/src/locales/messages.fr.xlf
  49. 330
      apps/client/src/locales/messages.it.xlf
  50. 8737
      apps/client/src/locales/messages.ko.xlf
  51. 330
      apps/client/src/locales/messages.nl.xlf
  52. 330
      apps/client/src/locales/messages.pl.xlf
  53. 330
      apps/client/src/locales/messages.pt.xlf
  54. 330
      apps/client/src/locales/messages.tr.xlf
  55. 330
      apps/client/src/locales/messages.uk.xlf
  56. 327
      apps/client/src/locales/messages.xlf
  57. 330
      apps/client/src/locales/messages.zh.xlf
  58. 1
      libs/common/src/lib/config.ts
  59. 28
      libs/common/src/lib/helper.ts
  60. 6
      libs/common/src/lib/interfaces/info-item.interface.ts
  61. 8
      libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
  62. 2
      libs/common/src/lib/models/timeline-position.ts
  63. 9
      libs/ui/src/lib/accounts-table/accounts-table.component.html
  64. 6
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  65. 11
      libs/ui/src/lib/benchmark/benchmark.component.ts
  66. 7
      libs/ui/src/lib/holdings-table/holdings-table.component.html
  67. 4
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  68. 19
      libs/ui/src/lib/services/data.service.ts
  69. 20
      package-lock.json
  70. 6
      package.json

46
CHANGELOG.md

@ -13,16 +13,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 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
- Set the active sort column in the accounts table component
- Deprecated `activities` in the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol`
- Moved the admin service to `@ghostfolio/ui/services`
- Moved the data service to `@ghostfolio/ui/services`
- Refactored the dividend import
- Refreshed the cryptocurrencies list
### 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

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

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(

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 { 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,34 @@ 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,
averagePrice: item.averagePrice,
currency: item.currency,
dataSource: item.dataSource,
dividend: totalDividend,
dividendInBaseCurrency: totalDividendInBaseCurrency,
fee: item.fee,
feeInBaseCurrency: item.feeInBaseCurrency,
firstBuyDate: item.firstBuyDate,
grossPerformance: !hasErrors ? (grossPerformance ?? null) : null,
grossPerformancePercentage: !hasErrors
@ -426,9 +429,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 +933,7 @@ export abstract class PortfolioCalculator {
for (const {
date,
fee,
feeInBaseCurrency,
quantity,
SymbolProfile,
tags,
@ -995,6 +998,8 @@ export abstract class PortfolioCalculator {
: investment.div(newQuantity).abs(),
dividend: new Big(0),
fee: oldAccumulatedSymbol.fee.plus(fee),
feeInBaseCurrency:
oldAccumulatedSymbol.feeInBaseCurrency.plus(feeInBaseCurrency),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
includeInHoldings: oldAccumulatedSymbol.includeInHoldings,
quantity: newQuantity,
@ -1007,6 +1012,7 @@ export abstract class PortfolioCalculator {
currency,
dataSource,
fee,
feeInBaseCurrency,
skipErrors,
symbol,
tags,

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

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

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

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

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

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

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

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

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 { 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,72 @@ 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>({
averagePrice: new Big(1),
currency: 'USD',
dataSource: DataSource.YAHOO,
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,

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

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

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

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

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

3
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,
@ -129,7 +130,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'],

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

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

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

@ -792,7 +792,7 @@ export class PortfolioService {
averagePrice,
currency,
dividendInBaseCurrency,
fee,
feeInBaseCurrency,
firstBuyDate,
grossPerformance,
grossPerformancePercentage,
@ -920,19 +920,15 @@ export class PortfolioService {
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:

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

@ -22,6 +22,7 @@
"2015": "2015 coin",
"2024": "2024",
"2025": "2025 TOKEN",
"2026": "2026",
"2049": "TOKEN 2049",
"2192": "LERNITAS",
"4444": "4444 Meme",
@ -83,6 +84,7 @@
"1UP": "Uptrennd",
"1WO": "1World",
"2022M": "2022MOON",
"2026MEMECLUB": "2026",
"20EX": "20ex",
"21BTC": "21.co Wrapped BTC",
"21X": "21X",
@ -177,11 +179,13 @@
"AAI": "AutoAir AI",
"AAPLX": "Apple xStock",
"AAPX": "AMPnet",
"AARBWBTC": "Aave Arbitrum WBTC",
"AARDY": "Baby Aardvark",
"AARK": "Aark",
"AART": "ALL.ART",
"AAST": "AASToken",
"AAT": "Agricultural Trade Chain",
"AAVAWBTC": "Aave aWBTC",
"AAVE": "Aave",
"AAVEE": "AAVE.e (Avalanche Bride)",
"AAVEGOTCHIFOMO": "Aavegotchi FOMO",
@ -295,7 +299,8 @@
"ADEL": "Akropolis Delphi",
"ADF": "Art de Finance",
"ADH": "Adhive",
"ADI": "Aditus",
"ADI": "ADI",
"ADITUS": "Aditus",
"ADIX": "Adix Token",
"ADK": "Aidos Kuneen",
"ADL": "Adel",
@ -412,6 +417,7 @@
"AGVC": "AgaveCoin",
"AGVE": "Agave",
"AGX": "Agricoin",
"AHARWBTC": "Aave Harmony WBTC",
"AHOO": "Ahoolee",
"AHT": "AhaToken",
"AI": "Sleepless",
@ -626,6 +632,7 @@
"ALITA": "Alita Network",
"ALITATOKEN": "Alita Token",
"ALIX": "AlinX",
"ALK": "Alkemi Network DAO Token",
"ALKI": "Alkimi",
"ALKIMI": "ALKIMI",
"ALLBI": "ALL BEST ICO",
@ -639,6 +646,7 @@
"ALMANAK": "Almanak",
"ALMC": "Awkward Look Monkey Club",
"ALME": "Alita",
"ALMEELA": "Almeela",
"ALMOND": "Almond",
"ALN": "Aluna",
"ALNV1": "Aluna v1",
@ -690,6 +698,7 @@
"AMAL": "AMAL",
"AMAPT": "Amnis Finance",
"AMATEN": "Amaten",
"AMATO": "AMATO",
"AMAZINGTEAM": "AmazingTeamDAO",
"AMB": "AirDAO",
"AMBER": "AmberCoin",
@ -825,6 +834,7 @@
"AOK": "AOK",
"AOL": "AOL (America Online)",
"AOP": "Ark Of Panda",
"AOPTWBTC": "Aave Optimism WBTC",
"AOS": "AOS",
"AOT": "Age of Tanks",
"AP": "America Party",
@ -861,6 +871,7 @@
"APOL": "Apollo FTW",
"APOLL": "Apollon Limassol",
"APOLLO": "Apollo Crypto",
"APOLWBTC": "Aave Polygon WBTC",
"APP": "Moon App",
"APPA": "Dappad",
"APPC": "AppCoins",
@ -914,6 +925,7 @@
"ARAW": "Araw",
"ARB": "Arbitrum",
"ARBI": "Arbipad",
"ARBINU": "ArbInu",
"ARBIT": "Arbit Coin",
"ARBP": "ARB Protocol",
"ARBS": "Arbswap",
@ -1041,6 +1053,7 @@
"ASBNB": "Astherus Staked BNB",
"ASC": "All InX SMART CHAIN",
"ASCEND": "Ascend",
"ASCN": "AlphaScan",
"ASD": "AscendEX Token",
"ASDEX": "AstraDEX",
"ASEED": "aUSD SEED (Acala)",
@ -1133,6 +1146,7 @@
"ATLA": "Atleta Network",
"ATLAS": "Star Atlas",
"ATLASD": "Atlas DEX",
"ATLASOFUSA": "Atlas",
"ATLX": "Atlantis Loans Polygon",
"ATM": "Atletico de Madrid Fan Token",
"ATMA": "ATMA",
@ -1173,6 +1187,7 @@
"AUCO": "Advanced United Continent",
"AUCTION": "Bounce",
"AUDC": "Aussie Digital",
"AUDD": "Australian Digital Dollar",
"AUDF": "Forte AUD",
"AUDIO": "Audius",
"AUDM": "Macropod Stablecoin",
@ -1260,6 +1275,7 @@
"AWARE": "ChainAware.ai",
"AWARETOKEN": "AWARE",
"AWAX": "AWAX",
"AWBTC": "Aave interest bearing WBTC",
"AWC": "Atomic Wallet Coin",
"AWE": "AWE Network",
"AWK": "Awkward Monkey Base",
@ -1302,6 +1318,7 @@
"AXT": "AIX",
"AXYS": "Axys",
"AYA": "Aryacoin",
"AYFI": "Aave YFI",
"AYNI": "Ayni Gold",
"AZ": "Azbit",
"AZA": "Kaliza",
@ -1314,6 +1331,7 @@
"AZR": "Azure",
"AZU": "Azultec",
"AZUKI": "Azuki",
"AZUKI2": "AZUKI 2.0",
"AZUKIDAO": "AzukiDAO",
"AZUM": "Azuma Coin",
"AZUR": "Azuro Protocol",
@ -1535,6 +1553,7 @@
"BAOM": "Battle of Memes",
"BAOS": "BaoBaoSol",
"BAOV1": "BaoToken v1",
"BAP3X": "bAP3X",
"BAR": "FC Barcelona Fan Token",
"BARA": "Capybara",
"BARAKATUH": "Barakatuh",
@ -1678,6 +1697,7 @@
"BCOINM": "Bomb Crypto (MATIC)",
"BCOINSOL": "Bomb Crypto (SOL)",
"BCOINTON": "Bomb Crypto (TON)",
"BCONG": "BabyCong",
"BCOQ": "BLACK COQINU",
"BCP": "BlockChainPeople",
"BCPAY": "Bitcashpay",
@ -1735,6 +1755,7 @@
"BEATAI": "eBeat AI",
"BEATLES": "JohnLennonC0IN",
"BEATS": "Sol Beats",
"BEATSWAP": "BeatSwap",
"BEATTOKEN": "BEAT Token",
"BEAVER": "beaver",
"BEB1M": "BeB",
@ -1851,6 +1872,7 @@
"BFLOKI": "BurnFloki",
"BFLY": "Butterfly Protocol",
"BFM": "BenefitMine",
"BFR": "Buffer Token",
"BFT": "BF Token",
"BFTB": "Brazil Fan Token",
"BFTC": "BITS FACTOR",
@ -2098,6 +2120,7 @@
"BLACKSALE": "Black Sale",
"BLACKST": "Black Stallion",
"BLACKSWAN": "BlackSwan AI",
"BLACKWHALE": "The Black Whale",
"BLADE": "BladeGames",
"BLADEW": "BladeWarrior",
"BLAKEBTC": "BlakeBitcoin",
@ -2143,7 +2166,8 @@
"BLOB": "B.O.B the Blob",
"BLOBERC20": "Blob",
"BLOC": "Blockcloud",
"BLOCK": "Blockasset",
"BLOCK": "Block",
"BLOCKASSET": "Blockasset",
"BLOCKB": "Block Browser",
"BLOCKBID": "Blockbid",
"BLOCKF": "Block Farm Club",
@ -2218,6 +2242,7 @@
"BM": "BitMoon",
"BMAGA": "Baby Maga",
"BMARS": "Binamars",
"BMAX": "BMAX",
"BMB": "Beamable Network Token",
"BMBO": "Bamboo Coin",
"BMC": "Blackmoon Crypto",
@ -2495,6 +2520,7 @@
"BOWSC": "BowsCoin",
"BOWSER": "Bowser",
"BOX": "DeBoxToken",
"BOXABL": "BOXABL",
"BOXCAT": "BOXCAT",
"BOXETH": "Cat-in-a-Box Ether",
"BOXT": "BOX Token",
@ -2579,6 +2605,7 @@
"BRETTGOLD": "Brett Gold",
"BRETTONETH": "Brett ETH",
"BRETTSUI": "Brett (brettsui.com)",
"BREV": "Brevis Token",
"BREW": "CafeSwap Token",
"BREWERY": "Brewery Consortium Coin",
"BREWLABS": "Brewlabs",
@ -2737,6 +2764,7 @@
"BTCAS": "BitcoinAsia",
"BTCAT": "Bitcoin Cat",
"BTCB": "Bitcoin BEP2",
"BTCBAM": "BitCoin Bam",
"BTCBASE": "Bitcoin on Base",
"BTCBR": "Bitcoin BR",
"BTCBRV1": "Bitcoin BR v1",
@ -3126,7 +3154,8 @@
"CASPER": "Casper DeFi",
"CASPERTOKEN": "Casper Token",
"CASPUR": "Caspur Zoomies",
"CAST": "Castello Coin",
"CAST": "CAST ORACLES",
"CASTELLOCOIN": "Castello Coin",
"CASTLE": "bitCastle",
"CAT": "Simon's Cat",
"CATA": "CATAMOTO",
@ -3236,6 +3265,7 @@
"CBX": "CropBytes",
"CBXRP": "Coinbase Wrapped XRP",
"CBY": "Carbify",
"CBYTE": "CBYTE",
"CC": "Canton Coin",
"CC10": "Cryptocurrency Top 10 Tokens Index",
"CCA": "CCA",
@ -3310,6 +3340,7 @@
"CENTRA": "Centra",
"CENTS": "Centience",
"CENX": "Centcex",
"CEO": "CEO",
"CEODOGE": "CEO DOGE",
"CERBER": "CERBEROGE",
"CERE": "Cere Network",
@ -3695,7 +3726,8 @@
"COC": "Coin of the champions",
"COCAINE": "THE GOOD STUFF",
"COCK": "Shibacock",
"COCO": "COCO COIN",
"COCO": "coco",
"COCOCOIN": "COCO COIN",
"COCONUT": "Coconut",
"COCOR": "Cocoro",
"COCORO": "Cocoro",
@ -3726,6 +3758,7 @@
"COI": "Coinnec",
"COINAI": "Coinbase AI Agent",
"COINB": "Coinbidex",
"COINBANK": "CoinBank",
"COINBT": "CoinBot",
"COINBUCK": "Coinbuck",
"COINDEALTOKEN": "CoinDeal Token",
@ -3750,6 +3783,7 @@
"COLA": "Cola",
"COLISEUM": "Coliseum",
"COLL": "Collateral Pay",
"COLLAB": "Collab.Land",
"COLLAR": "PolyPup Finance",
"COLLAT": "Collaterize",
"COLLE": "Collective Care",
@ -3970,6 +4004,7 @@
"CREPECOIN": "Crepe Coin",
"CRES": "Cresio",
"CRESV1": "Cresio v1",
"CRETA": "Creta World",
"CREV": "CryptoRevolution",
"CREVA": "Creva Coin",
"CREW": "CREW INU",
@ -4778,8 +4813,10 @@
"DINGER": "Dinger Token",
"DINGO": "Dingocoin",
"DINNER": "Trump Dinner",
"DINO": "DinoLFG",
"DINO": "DINO",
"DINOLFG": "DinoLFG",
"DINOS": "Dinosaur Inu",
"DINOSOL": "DINOSOL",
"DINOSWAP": "DinoSwap",
"DINT": "DinarTether",
"DINU": "Dogey-Inu",
@ -4974,6 +5011,7 @@
"DOGEZILLA": "DogeZilla",
"DOGEZILLAV1": "DogeZilla v1",
"DOGG": "Doggo",
"DOGGO": "DOGGO",
"DOGGS": "Doggensnout",
"DOGGY": "Doggy",
"DOGGYCOIN": "DOGGY",
@ -5487,6 +5525,7 @@
"EGGC": "EggCoin",
"EGGMAN": "Eggman Inu",
"EGGP": "Eggplant Finance",
"EGGT": "Egg N Partners",
"EGGY": "EGGY",
"EGI": "eGame",
"EGL": "The Eagle Of Truth",
@ -5711,6 +5750,7 @@
"EPETS": "Etherpets",
"EPIC": "Epic Chain",
"EPICCASH": "Epic Cash",
"EPICV1": "Ethernity Chain",
"EPIK": "EPIK Token",
"EPIKO": "Epiko",
"EPIX": "Byepix",
@ -6140,6 +6180,7 @@
"FCTC": "FaucetCoin",
"FCTR": "FactorDAO",
"FDC": "Fidance",
"FDGC": "FINTECH DIGITAL GOLD COIN",
"FDLS": "FIDELIS",
"FDM": "Fandom",
"FDO": "Firdaos",
@ -6440,7 +6481,9 @@
"FOFARIO": "Fofar",
"FOFO": "FOFO",
"FOFOTOKEN": "FOFO Token",
"FOG": "FOGnet",
"FOGE": "Fat Doge",
"FOGV1": "FOGnet v1",
"FOIN": "Foin",
"FOL": "Folder Protocol",
"FOLD": "Manifold Finance",
@ -6709,6 +6752,7 @@
"FWB": "Friends With Benefits Pro",
"FWBV1": "Friends With Benefits Pro v1",
"FWC": "Qatar 2022",
"FWCL": "Legends",
"FWH": "FigureWifHat",
"FWOG": "Fwog",
"FWT": "Freeway Token",
@ -7042,6 +7086,7 @@
"GIGASWAP": "GigaSwap",
"GIGGLE": "Giggle Fund",
"GIGGLEACADEMY": "Giggle Academy",
"GIGL": "GIGGLE PANDA",
"GIGS": "Climate101",
"GIGX": "GigXCoin",
"GIKO": "Giko Cat",
@ -7558,6 +7603,7 @@
"HACHIKO": "Hachiko Inu Token",
"HACHIONB": "Hachi On Base",
"HACK": "HACK",
"HADES": "Hades",
"HAEDAL": "Haedal Protocol",
"HAGGIS": "New Born Haggis Pygmy Hippo",
"HAHA": "Hasaki",
@ -7772,6 +7818,7 @@
"HIENS3": "hiENS3",
"HIENS4": "hiENS4",
"HIFI": "Hifi Finance",
"HIFIDENZA": "hiFIDENZA",
"HIFLUF": "hiFLUF",
"HIFRIENDS": "hiFRIENDS",
"HIGAZERS": "hiGAZERS",
@ -7811,6 +7858,7 @@
"HITBTC": "HitBTC Token",
"HITOP": "Hitop",
"HIUNDEAD": "hiUNDEAD",
"HIVALHALLA": "hiVALHALLA",
"HIVE": "Hive",
"HIVP": "HiveSwap",
"HIX": "HELIX Orange",
@ -7984,6 +8032,7 @@
"HSN": "Hyper Speed Network",
"HSOL": "Helius Staked SOL",
"HSP": "Horse Power",
"HSR": "Hshare",
"HSS": "Hashshare",
"HST": "Decision Token",
"HSUI": "Suicune",
@ -8005,6 +8054,7 @@
"HTN": "Hoosat Network",
"HTO": "Heavenland HTO",
"HTR": "Hathor",
"HTS": "Home3",
"HTT": "Hello Art",
"HTX": "HTX",
"HTZ": "Hertz Network",
@ -8427,6 +8477,7 @@
"IQN": "IQeon",
"IQQ": "Iqoniq",
"IQT": "IQ Protocol",
"IR": "Infrared Governance Token",
"IRA": "Diligence",
"IRC": "IRIS",
"IRENA": "Irena Coin Apps",
@ -8558,6 +8609,7 @@
"JAWN": "Long Jawn Silvers",
"JAWS": "AutoShark",
"JAY": "Jaypeggers",
"JBC": "Japan Brand Coin",
"JBO": "JBOX",
"JBOT": "JACKBOT",
"JBS": "JumBucks Coin",
@ -8596,6 +8648,7 @@
"JERRYINUCOM": "Jerry Inu",
"JES": "Jesus",
"JESSE": "jesse",
"JESSECOIN": "jesse",
"JEST": "Jester",
"JESUS": "Jesus Coin",
"JET": "Jet Protocol",
@ -8624,6 +8677,7 @@
"JIM": "Jim",
"JIN": "JinPeng",
"JIND": "JINDO INU",
"JINDO": "JINDOGE",
"JINDOGE": "Jindoge",
"JIO": "JIO Token",
"JITOSOL": "Jito Staked SOL",
@ -8865,6 +8919,7 @@
"KDC": "Klondike Coin",
"KDG": "Kingdom Game 4.0",
"KDIA": "KDIA COIN",
"KDK": "Kodiak Token",
"KDOE": "Kudoe",
"KDOGE": "KingDoge",
"KDT": "Kenyan Digital Token",
@ -8926,6 +8981,7 @@
"KGC": "Krypton Galaxy Coin",
"KGEN": "KGeN",
"KGO": "Kiwigo",
"KGST": "KGST",
"KGT": "Kaby Gaming Token",
"KHAI": "khai",
"KHEOWZOO": "khaokheowzoo",
@ -9074,6 +9130,7 @@
"KNUT": "Knut From Zoo",
"KNUXX": "Knuxx Bully of ETH",
"KNW": "Knowledge",
"KO": "Kyuzo's Friends",
"KOAI": "KOI",
"KOALA": "KOALA",
"KOBAN": "KOBAN",
@ -9089,6 +9146,7 @@
"KOII": "Koii",
"KOIN": "Koinos",
"KOINB": "KoinBülteni Token",
"KOINDEX": "KOIN",
"KOINETWORK": "Koi Network",
"KOIP": "KoiPond",
"KOJI": "Koji",
@ -9548,8 +9606,9 @@
"LIQUIDIUM": "LIQUIDIUM•TOKEN",
"LIR": "Let it Ride",
"LIS": "Realis Network",
"LISA": "Lisa Simpson",
"LISA": "LISA Token",
"LISAS": "Lisa Simpson",
"LISASIMPSONCLUB": "Lisa Simpson",
"LIST": "KList Protocol",
"LISTA": "Lista DAO",
"LISTEN": "Listen",
@ -9732,6 +9791,7 @@
"LQT": "Lifty",
"LQTY": "Liquity",
"LRC": "Loopring",
"LRCV1": "Loopring v1",
"LRDS": "BLOCKLORDS",
"LRG": "Largo Coin",
"LRN": "Loopring [NEO]",
@ -9950,6 +10010,7 @@
"MAGICK": "Cosmic Universe Magick",
"MAGICV": "Magicverse",
"MAGIK": "Magik Finance",
"MAGMA": "MAGMA",
"MAGN": "Magnate Finance",
"MAGNE": "Magnetix",
"MAGNET": "Yield Magnet",
@ -10198,6 +10259,7 @@
"MCU": "MediChain",
"MCUSD": "Moola Celo USD",
"MCV": "MCV Token",
"MCX": "MachiX Token",
"MD": "MetaDeck",
"MDA": "Moeda",
"MDAI": "MindAI",
@ -10366,6 +10428,7 @@
"METANIAV1": "METANIAGAMES",
"METANO": "Metano",
"METAPK": "Metapocket",
"METAPLACE": "Metaplace",
"METAQ": "MetaQ",
"METAS": "Metaseer",
"METAT": "MetaTrace",
@ -10510,6 +10573,7 @@
"MIMO": "MIMO Parallel Governance Token",
"MIN": "MINDOL",
"MINA": "Mina Protocol",
"MINAR": "Miner Arena",
"MINC": "MinCoin",
"MIND": "Morpheus Labs",
"MINDBODY": "Mind Body Soul",
@ -10911,7 +10975,7 @@
"MPAA": "MPAA",
"MPAD": "MultiPad",
"MPAY": "Menapay",
"MPC": "Metaplace",
"MPC": "Partisia Blockchain",
"MPD": "Metapad",
"MPG": "Max Property Group",
"MPH": "Morpher",
@ -11010,6 +11074,7 @@
"MTHB": "MTHAIBAHT",
"MTHD": "Method Finance",
"MTHN": "MTH Network",
"MTHT": "MetaHint",
"MTIK": "MatikaToken",
"MTIX": "Matrix Token",
"MTK": "Moya Token",
@ -11175,6 +11240,7 @@
"N3": "Network3",
"N3DR": "NeorderDAO ",
"N3ON": "N3on",
"N4T": "Nobel For Trump",
"N64": "N64",
"N7": "Number7",
"N8V": "NativeCoin",
@ -11722,6 +11788,7 @@
"NWIF": "neirowifhat",
"NWP": "NWPSolution",
"NWS": "Nodewaves",
"NXA": "NEXA Agent",
"NXC": "Nexium",
"NXD": "Nexus Dubai",
"NXDT": "NXD Next",
@ -11766,7 +11833,8 @@
"OAS": "Oasis City",
"OASC": "Oasis City",
"OASI": "Oasis Metaverse",
"OASIS": "Oasis",
"OASIS": "OASIS",
"OASISPLATFORM": "Oasis",
"OAT": "OAT Network",
"OATH": "OATH Protocol",
"OAX": "Oax",
@ -11983,12 +12051,14 @@
"ONUS": "ONUS",
"ONX": "OnX.finance",
"OOB": "Oobit",
"OOBV1": "Oobit",
"OOE": "OpenOcean",
"OOFP": "OOFP",
"OOGI": "OOGI",
"OOKI": "Ooki",
"OOKS": "Onooks",
"OOM": "OomerBot",
"OOOO": "oooo",
"OOPS": "OOPS",
"OORC": "Orbit Bridge Klaytn Orbit Chain",
"OORT": "OORT",
@ -12838,7 +12908,7 @@
"PMOON": "Pookimoon",
"PMPY": "Prometheum Prodigy",
"PMR": "Pomerium Utility Token",
"PMT": "POWER MARKET",
"PMT": "Public Masterpiece Token",
"PMTN": "Peer Mountain",
"PMX": "Phillip Morris xStock",
"PNB": "Pink BNB",
@ -13001,7 +13071,9 @@
"POUW": "Pouwifhat",
"POW": "PowBlocks",
"POWELL": "Jerome Powell",
"POWER": "Powerloom Token",
"POWER": "Power",
"POWERLOOM": "Powerloom Token",
"POWERMARKET": "POWER MARKET",
"POWR": "Power Ledger",
"POWSCHE": "Powsche",
"POX": "Monkey Pox",
@ -13269,6 +13341,7 @@
"PXL": "PIXEL",
"PXP": "PointPay",
"PXT": "Pixer Eternity",
"PYBOBO": "Capybobo",
"PYC": "PayCoin",
"PYE": "CreamPYE",
"PYI": "PYRIN",
@ -13470,6 +13543,7 @@
"RAIREFLEX": "Rai Reflex Index",
"RAISE": "Raise Token",
"RAIT": "Rabbitgame",
"RAITOKEN": "RAI",
"RAIZER": "RAIZER",
"RAK": "Rake Finance",
"RAKE": "Rake Coin",
@ -13497,10 +13571,11 @@
"RATOTHERAT": "Rato The Rat",
"RATS": "Rats",
"RATWIF": "RatWifHat",
"RAVE": "Ravendex",
"RAVE": "RaveDAO",
"RAVELOUS": "Ravelous",
"RAVEN": "Raven Protocol",
"RAVENCOINC": "Ravencoin Classic",
"RAVENDEX": "Ravendex",
"RAWDOG": "RawDog",
"RAWG": "RAWG",
"RAY": "Raydium",
@ -13810,7 +13885,8 @@
"RIVUS": "RivusDAO",
"RIYA": "Etheriya",
"RIZ": "Rivalz Network",
"RIZE": "Rizespor Token",
"RIZE": "RIZE",
"RIZESPOR": "Rizespor Token",
"RIZO": "HahaYes",
"RIZOLOL": "Rizo",
"RIZZ": "Rizz",
@ -13856,6 +13932,7 @@
"RNT": "REAL NIGGER TATE",
"RNTB": "BitRent",
"RNX": "ROONEX",
"ROA": "ROA CORE",
"ROAD": "ROAD",
"ROAM": "Roam Token",
"ROAR": "Alpha DEX",
@ -13993,6 +14070,7 @@
"RTM": "Raptoreum",
"RTR": "Restore The Republic",
"RTT": "Restore Truth Token",
"RTX": "RateX",
"RU": "RIFI United",
"RUBB": "Rubber Ducky Cult",
"RUBCASH": "RUBCASH",
@ -14283,7 +14361,7 @@
"SCOIN": "ShinCoin",
"SCONE": "Sportcash One",
"SCOOBY": "Scooby coin",
"SCOR": "Scorista",
"SCOR": "Scor",
"SCORE": "Scorecoin",
"SCOT": "Scotcoin",
"SCOTT": "Scottish",
@ -14377,6 +14455,7 @@
"SELFIEC": "Selfie Cat",
"SELFT": "SelfToken",
"SELLC": "Sell Token",
"SELO": "SELO+",
"SEM": "Semux",
"SEN": "Sentaro",
"SENA": "Ethena Staked ENA",
@ -14482,7 +14561,6 @@
"SHANG": "Shanghai Inu",
"SHAR": "Shark Cat",
"SHARBI": "SHARBI",
"SHARD": "ShardCoin",
"SHARDS": "WorldShards",
"SHARE": "Seigniorage Shares",
"SHARECHAIN": "ShareChain",
@ -14580,6 +14658,7 @@
"SHIRO": "Shiro Neko",
"SHIROSOL": "Shiro Neko (shirosol.online)",
"SHIRYOINU": "Shiryo-Inu",
"SHISA": "SHISA",
"SHISHA": "Shisha Coin",
"SHIT": "I will poop it NFT",
"SHITC": "Shitcoin",
@ -14628,9 +14707,11 @@
"SHUFFLE": "SHUFFLE!",
"SHVR": "Shivers",
"SHX": "Stronghold Token",
"SHXV1": "Stronghold Token v1",
"SHY": "Shytoshi Kusama",
"SHYTCOIN": "ShytCoin",
"SI": "Siren",
"SI14": "Si14",
"SIACLASSIC": "SiaClassic",
"SIB": "SibCoin",
"SIBA": "SibaInu",
@ -14831,6 +14912,7 @@
"SMARTLOX": "SmartLOX",
"SMARTM": "SmartMesh",
"SMARTMEME": "SmartMEME",
"SMARTMFG": "Smart MFG",
"SMARTNFT": "SmartNFT",
"SMARTO": "smARTOFGIVING",
"SMARTSHARE": "Smartshare",
@ -15040,6 +15122,7 @@
"SOLNAV": "SOLNAV AI",
"SOLNIC": "Solnic",
"SOLO": "Sologenic",
"SOLOM": "Solomon",
"SOLOR": "Solordi",
"SOLP": "SolPets",
"SOLPAD": "Solpad Finance",
@ -15332,6 +15415,7 @@
"STACS": "STACS Token",
"STAFIRETH": "StaFi Staked ETH",
"STAGE": "Stage",
"STAI": "StereoAI",
"STAK": "Jigstack",
"STAKE": "xDai Chain",
"STAKEDETH": "StakeHound Staked Ether",
@ -15422,6 +15506,7 @@
"STIMA": "STIMA",
"STING": "Sting",
"STINJ": "Stride Staked INJ",
"STIPS": "Stips",
"STITCH": "Stitch",
"STIX": "STIX",
"STJUNO": "Stride Staked JUNO",
@ -15597,6 +15682,7 @@
"SUPERBONK": "SUPER BONK",
"SUPERC": "SuperCoin",
"SUPERCAT": "SUPERCAT",
"SUPERCYCLE": "Crypto SuperCycle",
"SUPERDAPP": "SuperDapp",
"SUPERF": "SUPER FLOKI",
"SUPERGROK": "SuperGrok",
@ -15855,6 +15941,7 @@
"TBILL": "OpenEden T-Bills",
"TBILLV1": "OpenEden T-Bills v1",
"TBIS": "TBIS token",
"TBK": "TBK Token",
"TBL": "Tombola",
"TBLLX": "TBLL xStock",
"TBR": "Tuebor",
@ -15902,7 +15989,8 @@
"TDROP": "ThetaDrop",
"TDS": "TokenDesk",
"TDX": "Tidex Token",
"TEA": "TeaDAO",
"TEA": "TeaFi Token",
"TEADAO": "TeaDAO",
"TEAM": "TeamUP",
"TEARS": "Liberals Tears",
"TEC": "TeCoin",
@ -15933,7 +16021,7 @@
"TEMM": "TEM MARKET",
"TEMP": "Tempus",
"TEMPLE": "TempleDAO",
"TEN": "Tokenomy",
"TEN": "TEN",
"TEND": "Tendies",
"TENDIE": "TendieSwap",
"TENET": "TENET",
@ -15947,8 +16035,8 @@
"TEQ": "Teq Network",
"TER": "TerraNovaCoin",
"TERA": "TERA",
"TERA2": "Terareum",
"TERADYNE": "Teradyne",
"TERAR": "Terareum",
"TERAV1": "Terareum v1",
"TERAWATT": "Terawatt",
"TERM": "Terminal of Simpson",
@ -15975,6 +16063,7 @@
"TETSUO": "Tetsuo Coin",
"TETU": "TETU",
"TEVA": "Tevaera",
"TEVI": "TEVI Coin",
"TEW": "Trump in a memes world",
"TEX": "Terrax",
"TF47": "Trump Force 47",
@ -15988,7 +16077,7 @@
"TFT": "The Famous Token",
"TFUEL": "Theta Fuel",
"TGAME": "TrueGame",
"TGC": "TigerCoin",
"TGC": "TG.Casino",
"TGCC": "TheGCCcoin",
"TGPT": "Trading GPT",
"TGRAM": "TG20 TGram",
@ -16026,6 +16115,7 @@
"THEOS": "Theos",
"THEP": "The Protocol",
"THEPLAY": "PLAY",
"THEREALCHAIN": "REAL",
"THERESAMAY": "Theresa May Coin",
"THES": "The Standard Protocol (USDS)",
"THESTANDARD": "Standard Token",
@ -16051,6 +16141,7 @@
"THOR": "THORSwap",
"THOREUM": "Thoreum V3",
"THP": "TurboHigh Performance",
"THQ": "Theoriq Token",
"THR": "Thorecoin",
"THREE": "Three Protocol Token ",
"THRT": "ThriveToken",
@ -16078,6 +16169,7 @@
"TIG": "Tigereum",
"TIGER": "TIGER",
"TIGERC": "TigerCash",
"TIGERCOIN": "TigerCoin",
"TIGERCV1": "TigerCash v1",
"TIGERMOON": "TigerMoon",
"TIGERSHARK": "Tiger Shark",
@ -16154,6 +16246,7 @@
"TME": "Timereum",
"TMED": "MDsquare",
"TMFT": "Turkish Motorcycle Federation",
"TMG": "T-mac DAO",
"TMN": "TranslateMe",
"TMNG": "TMN Global",
"TMNT": "TMNT",
@ -16193,6 +16286,7 @@
"TOKC": "Tokyo Coin",
"TOKE": "Tokemak",
"TOKEN": "TokenFi",
"TOKENOMY": "Tokenomy",
"TOKENPLACE": "Tokenplace",
"TOKENSTARS": "TokenStars",
"TOKERO": "TOKERO LevelUP Token",
@ -16312,6 +16406,7 @@
"TRADE": "Polytrade",
"TRADEBOT": "TradeBot",
"TRADECHAIN": "Trade Chain",
"TRADETIDE": "Trade Tide Token",
"TRADEX": "TradeX AI",
"TRADOOR": "Tradoor",
"TRAI": "Trackgood AI",
@ -16361,6 +16456,7 @@
"TRIAS": "Trias",
"TRIBE": "Tribe",
"TRIBETOKEN": "TribeToken",
"TRIBEX": "Tribe Token",
"TRIBL": "Tribal Token",
"TRICK": "TrickyCoin",
"TRICKLE": "Trickle",
@ -16882,6 +16978,7 @@
"USDI": "Interest Protocol USDi",
"USDJ": "USDJ",
"USDK": "USDK",
"USDKG": "USDKG",
"USDL": "Lift Dollar",
"USDM": "Mountain Protocol",
"USDMA": "USD mars",
@ -16946,6 +17043,7 @@
"UT": "Ulord",
"UTBAI": "UTB.ai",
"UTC": "UltraCoin",
"UTED": "United",
"UTG": "UltronGlow",
"UTH": "Uther",
"UTHR": "Utherverse Xaeon",
@ -16998,6 +17096,7 @@
"VALU": "Value",
"VALUE": "Value Liquidity",
"VALYR": "Valyr",
"VAM": "Vitalum",
"VAMPIRE": "Vampire Inu",
"VAN": "Vanspor Token",
"VANA": "Vana",
@ -17206,6 +17305,7 @@
"VIZ": "VIZ Token",
"VIZION": "ViZion Protocol",
"VIZSLASWAP": "VizslaSwap",
"VK": "VK Token",
"VKNF": "VKENAF",
"VLC": "Volcano Uni",
"VLDY": "Validity",
@ -17263,6 +17363,7 @@
"VON": "Vameon",
"VONE": "Vone",
"VONSPEED": "Andrea Von Speed",
"VOOI": "VOOI",
"VOOT": "VootCoin",
"VOOZ": "Vooz Coin",
"VOPO": "VOPO",
@ -17443,6 +17544,7 @@
"WATCH": "Yieldwatch",
"WATER": "Waterfall",
"WATERCOIN": "WATER",
"WATLAS": "Wrapped Star Atlas (Portal Bridge)",
"WATT": "WATTTON",
"WAVAX": "Wrapped AVAX",
"WAVES": "Waves",
@ -17524,6 +17626,7 @@
"WEBSIM": "The Css God by Virtuals",
"WEBSS": "Websser",
"WEC": "Whole Earth Coin",
"WECAN": "Wecan Group",
"WECO": "WECOIN",
"WED": "Wednesday Inu",
"WEEBS": "Weebs",
@ -17561,8 +17664,9 @@
"WEPC": "World Earn & Play Community",
"WEPE": "Wall Street Pepe",
"WERK": "Werk Family",
"WESHOWTOKEN": "WeShow Token",
"WEST": "Waves Enterprise",
"WET": "WeShow Token",
"WET": "HumidiFi Token",
"WETH": "WETH",
"WETHV1": "WETH v1",
"WETHW": "Wrapped EthereumPoW",
@ -17617,6 +17721,7 @@
"WHISKEY": "WHISKEY",
"WHITE": "WhiteRock",
"WHITEHEART": "Whiteheart",
"WHITEWHALE": "The White Whale",
"WHL": "WhaleCoin",
"WHO": "Truwho",
"WHOLE": "Whole Network",
@ -17827,6 +17932,7 @@
"WPP": "Green Energy Token",
"WPR": "WePower",
"WQT": "Work Quest",
"WR": "White Rat",
"WRC": "Worldcore",
"WREACT": "Wrapped REACT",
"WRK": "BlockWRK",
@ -18005,6 +18111,7 @@
"XCHF": "CryptoFranc",
"XCHNG": "Chainge Finance",
"XCI": "Cannabis Industry Coin",
"XCL": "Xcellar",
"XCLR": "ClearCoin",
"XCM": "CoinMetro",
"XCN": "Onyxcoin",
@ -18043,7 +18150,8 @@
"XEC": "eCash",
"XED": "Exeedme",
"XEDO": "XedoAI",
"XEL": "Xel",
"XEL": "XELIS",
"XELCOIN": "Xel",
"XELS": "XELS Coin",
"XEM": "NEM",
"XEN": "XEN Crypto",
@ -18361,13 +18469,15 @@
"YEAI": "YE AI Agent",
"YEARN": "YearnTogether",
"YEC": "Ycash",
"YEE": "Yeeco",
"YEE": "Yee Token",
"YEECO": "Yeeco",
"YEED": "Yggdrash",
"YEEHAW": "YEEHAW",
"YEET": "Yeet",
"YEETI": "YEETI 液体",
"YEFI": "YeFi",
"YEL": "Yel.Finance",
"YELLOWWHALE": "The Yellow Whale",
"YELP": "Yelpro",
"YEON": "Yeon",
"YEPE": "Yellow Pepe",

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

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>

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

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();

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

@ -74,6 +74,70 @@
</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="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`,

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

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

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

@ -1,5 +1,4 @@
import {
Activity,
Benchmark,
DataProviderInfo,
EnhancedSymbolProfile,
@ -9,17 +8,18 @@ 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;
/** @deprecated use dateOfFirstActivity */
firstBuyDate: string;
grossPerformance: number;
grossPerformancePercent: number;
grossPerformancePercentWithCurrencyEffect: number;

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

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

9
libs/ui/src/lib/accounts-table/accounts-table.component.html

@ -13,7 +13,14 @@
}
<div class="overflow-x-auto">
<table class="gf-table w-100" mat-table matSort [dataSource]="dataSource">
<table
class="gf-table w-100"
mat-table
matSort
matSortActive="name"
matSortDirection="asc"
[dataSource]="dataSource"
>
<ng-container matColumnDef="status">
<th
*matHeaderCellDef

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

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) {

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.231.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
"version": "2.228.0",
"version": "2.231.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@ -56,7 +56,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",
@ -125,7 +125,7 @@
"@types/google-spreadsheet": "3.1.5",
"@types/jest": "30.0.0",
"@types/jsonpath": "0.2.4",
"@types/lodash": "4.17.20",
"@types/lodash": "4.17.21",
"@types/node": "22.15.17",
"@types/papaparse": "5.3.7",
"@types/passport-google-oauth20": "2.0.16",
@ -12455,9 +12455,9 @@
}
},
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==",
"dev": true,
"license": "MIT"
},
@ -16319,9 +16319,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": {

6
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.228.0",
"version": "2.231.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -100,7 +100,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",
@ -169,7 +169,7 @@
"@types/google-spreadsheet": "3.1.5",
"@types/jest": "30.0.0",
"@types/jsonpath": "0.2.4",
"@types/lodash": "4.17.20",
"@types/lodash": "4.17.21",
"@types/node": "22.15.17",
"@types/papaparse": "5.3.7",
"@types/passport-google-oauth20": "2.0.16",

Loading…
Cancel
Save