Compare commits

...

11 Commits

Author SHA1 Message Date
Thomas Kaul 07cf3c670f
Feature/add blog post: Ghostfolio 3 (#6678) 15 hours ago
Thomas Kaul c7e029bc38
Bugfix/prisma client initialization in admin service (#6764) 16 hours ago
Thomas Kaul 2abba414b0 Merge branch 'main' into next 17 hours ago
Thomas Kaul 5a7667adef
Task/upgrade nestjs to version 11.1.19 (#6756) 17 hours ago
Thomas Kaul a75a5dd3cd
Task/refresh cryptocurrencies list (20260421) (#6758) 17 hours ago
Thomas Kaul 9eecad153a
Task/upgrade Nx to version 22.6.5 (#6757) 18 hours ago
Thomas Kaul fb982df929
Bugfix/release dates in CHANGELOG.md (#6763) 19 hours ago
Thomas Kaul 8b9c8e04f2
Task/upgrade jsonpath to version 1.3.0 (#6755) 2 days ago
Thomas Kaul ef7df25496
Task/refactor subscription types (#6735) 2 days ago
Thomas Kaul ccd81bde4b
Task/update OSS Friends 20260421 (#6754) 2 days ago
Thomas Kaul a463b5511b
Task/upgrade countup.js to version 2.10.0 (#6636) 3 days ago
  1. 22
      CHANGELOG.md
  2. 5
      apps/api/src/app/access/access.controller.ts
  3. 316
      apps/api/src/app/admin/admin.service.ts
  4. 3
      apps/api/src/app/endpoints/public/public.controller.ts
  5. 4
      apps/api/src/app/endpoints/sitemap/sitemap.service.ts
  6. 12
      apps/api/src/app/portfolio/portfolio.controller.ts
  7. 7
      apps/api/src/app/user/user.service.ts
  8. 109
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  9. 4
      apps/api/src/middlewares/html-template.middleware.ts
  10. 7
      apps/api/src/services/data-provider/data-provider.service.ts
  11. 7
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  12. 21
      apps/client/src/app/pages/blog/2026/04/ghostfolio-3/ghostfolio-3-page.component.ts
  13. 281
      apps/client/src/app/pages/blog/2026/04/ghostfolio-3/ghostfolio-3-page.html
  14. 26
      apps/client/src/app/pages/blog/blog-page.html
  15. 9
      apps/client/src/app/pages/blog/blog-page.routes.ts
  16. 5
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  17. 3
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
  18. BIN
      apps/client/src/assets/images/blog/ghostfolio-3.jpg
  19. 7
      apps/client/src/assets/oss-friends.json
  20. 3698
      package-lock.json
  21. 42
      package.json

22
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Next ## Next
### Added
- Added a blog post: _Announcing Ghostfolio 3.0_
### Changed ### Changed
- Migrated from _Material Design_ 2 to _Material Design_ 3 - Migrated from _Material Design_ 2 to _Material Design_ 3
@ -16,7 +20,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Breaking Change**: The `sslmode=prefer` parameter in `DATABASE_URL` is no longer supported. Please update your environment variables (see `.env`) to use `sslmode=require` if _SSL_ is enabled or remove the `sslmode` parameter entirely if _SSL_ is not used. - **Breaking Change**: The `sslmode=prefer` parameter in `DATABASE_URL` is no longer supported. Please update your environment variables (see `.env`) to use `sslmode=require` if _SSL_ is enabled or remove the `sslmode` parameter entirely if _SSL_ is not used.
## 2.255.0 - 2026-03-20 ## Unreleased
### Changed
- Refreshed the cryptocurrencies list
- Upgraded `countup.js` from version `2.9.0` to `2.10.0`
- Upgraded `jsonpath` from version `1.2.1` to `1.3.0`
- Upgraded `nestjs` from version `11.1.14` to `11.1.19`
- Upgraded `Nx` from version `22.6.4` to `22.6.5`
## 2.255.0 - 2026-04-20
### Changed ### Changed
@ -33,7 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed the missing value column of the accounts table component on mobile - Fixed the missing value column of the accounts table component on mobile
## 2.254.0 - 2026-03-10 ## 2.254.0 - 2026-04-10
### Added ### Added
@ -51,7 +65,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the style of the activity type component - Improved the style of the activity type component
## 2.253.0 - 2026-03-06 ## 2.253.0 - 2026-04-06
### Added ### Added
@ -71,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed the allocations by ETF provider chart on the allocations page in the _Presenter View_ - Fixed the allocations by ETF provider chart on the allocations page in the _Presenter View_
- Fixed the allocations by platform chart on the allocations page in the _Presenter View_ - Fixed the allocations by platform chart on the allocations page in the _Presenter View_
## 2.252.0 - 2026-03-02 ## 2.252.0 - 2026-04-02
### Added ### Added

5
apps/api/src/app/access/access.controller.ts

@ -2,6 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos'; import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { Access } from '@ghostfolio/common/interfaces'; import { Access } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
@ -75,7 +76,7 @@ export class AccessController {
): Promise<AccessModel> { ): Promise<AccessModel> {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic' this.request.user.subscription.type === SubscriptionType.Basic
) { ) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),
@ -130,7 +131,7 @@ export class AccessController {
): Promise<AccessModel> { ): Promise<AccessModel> {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic' this.request.user.subscription.type === SubscriptionType.Basic
) { ) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN), getReasonPhrase(StatusCodes.FORBIDDEN),

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

@ -36,7 +36,6 @@ import {
BadRequestException, BadRequestException,
HttpException, HttpException,
Injectable, Injectable,
Logger,
NotFoundException NotFoundException
} from '@nestjs/common'; } from '@nestjs/common';
import { import {
@ -44,7 +43,6 @@ import {
AssetSubClass, AssetSubClass,
DataSource, DataSource,
Prisma, Prisma,
PrismaClient,
Property, Property,
SymbolProfile SymbolProfile
} from '@prisma/client'; } from '@prisma/client';
@ -280,186 +278,178 @@ export class AdminService {
const extendedPrismaClient = this.getExtendedPrismaClient(); const extendedPrismaClient = this.getExtendedPrismaClient();
try { const symbolProfileResult = await Promise.all([
const symbolProfileResult = await Promise.all([ extendedPrismaClient.symbolProfile.findMany({
extendedPrismaClient.symbolProfile.findMany({ skip,
skip, take,
take, where,
where, orderBy: [...orderBy, { id: sortDirection }],
orderBy: [...orderBy, { id: sortDirection }],
select: {
_count: {
select: {
activities: true,
watchedBy: true
}
},
activities: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
},
assetClass: true,
assetSubClass: true,
comment: true,
countries: true,
currency: true,
dataSource: true,
id: true,
isActive: true,
isUsedByUsersWithSubscription: true,
name: true,
scraperConfiguration: true,
sectors: true,
symbol: true,
SymbolProfileOverrides: true
}
}),
this.prismaService.symbolProfile.count({ where })
]);
const assetProfiles = symbolProfileResult[0];
let count = symbolProfileResult[1];
const lastMarketPrices = await this.prismaService.marketData.findMany({
distinct: ['dataSource', 'symbol'],
orderBy: { date: 'desc' },
select: { select: {
_count: {
select: {
activities: true,
watchedBy: true
}
},
activities: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
},
assetClass: true,
assetSubClass: true,
comment: true,
countries: true,
currency: true,
dataSource: true, dataSource: true,
marketPrice: true, id: true,
symbol: true isActive: true,
isUsedByUsersWithSubscription: true,
name: true,
scraperConfiguration: true,
sectors: true,
symbol: true,
SymbolProfileOverrides: true
}
}),
this.prismaService.symbolProfile.count({ where })
]);
const assetProfiles = symbolProfileResult[0];
let count = symbolProfileResult[1];
const lastMarketPrices = await this.prismaService.marketData.findMany({
distinct: ['dataSource', 'symbol'],
orderBy: { date: 'desc' },
select: {
dataSource: true,
marketPrice: true,
symbol: true
},
where: {
dataSource: {
in: assetProfiles.map(({ dataSource }) => {
return dataSource;
})
}, },
where: { symbol: {
dataSource: { in: assetProfiles.map(({ symbol }) => {
in: assetProfiles.map(({ dataSource }) => { return symbol;
return dataSource; })
})
},
symbol: {
in: assetProfiles.map(({ symbol }) => {
return symbol;
})
}
} }
}); }
});
const lastMarketPriceMap = new Map<string, number>(); const lastMarketPriceMap = new Map<string, number>();
for (const { dataSource, marketPrice, symbol } of lastMarketPrices) { for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
lastMarketPriceMap.set( lastMarketPriceMap.set(
getAssetProfileIdentifier({ dataSource, symbol }), getAssetProfileIdentifier({ dataSource, symbol }),
marketPrice marketPrice
); );
} }
let marketData: AdminMarketDataItem[] = await Promise.all(
assetProfiles.map(
async ({
_count,
activities,
assetClass,
assetSubClass,
comment,
countries,
currency,
dataSource,
id,
isActive,
isUsedByUsersWithSubscription,
name,
sectors,
symbol,
SymbolProfileOverrides
}) => {
let countriesCount = countries ? Object.keys(countries).length : 0;
const lastMarketPrice = lastMarketPriceMap.get(
getAssetProfileIdentifier({ dataSource, symbol })
);
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
let sectorsCount = sectors ? Object.keys(sectors).length : 0;
if (SymbolProfileOverrides) {
assetClass = SymbolProfileOverrides.assetClass ?? assetClass;
assetSubClass =
SymbolProfileOverrides.assetSubClass ?? assetSubClass;
if (
(SymbolProfileOverrides.countries as unknown as Prisma.JsonArray)
?.length > 0
) {
countriesCount = (
SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
).length;
}
let marketData: AdminMarketDataItem[] = await Promise.all( name = SymbolProfileOverrides.name ?? name;
assetProfiles.map(
async ({ if (
_count, (SymbolProfileOverrides.sectors as unknown as Sector[])?.length >
activities, 0
) {
sectorsCount = (
SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray
).length;
}
}
return {
assetClass, assetClass,
assetSubClass, assetSubClass,
comment, comment,
countries, countriesCount,
currency, currency,
dataSource, dataSource,
id, id,
isActive, isActive,
isUsedByUsersWithSubscription, lastMarketPrice,
marketDataItemCount,
name, name,
sectors, sectorsCount,
symbol, symbol,
SymbolProfileOverrides activitiesCount: _count.activities,
}) => { date: activities?.[0]?.date,
let countriesCount = countries ? Object.keys(countries).length : 0; isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription,
watchedByCount: _count.watchedBy
const lastMarketPrice = lastMarketPriceMap.get( };
getAssetProfileIdentifier({ dataSource, symbol })
);
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
let sectorsCount = sectors ? Object.keys(sectors).length : 0;
if (SymbolProfileOverrides) {
assetClass = SymbolProfileOverrides.assetClass ?? assetClass;
assetSubClass =
SymbolProfileOverrides.assetSubClass ?? assetSubClass;
if (
(
SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
)?.length > 0
) {
countriesCount = (
SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
).length;
}
name = SymbolProfileOverrides.name ?? name;
if (
(SymbolProfileOverrides.sectors as unknown as Sector[])
?.length > 0
) {
sectorsCount = (
SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray
).length;
}
}
return {
assetClass,
assetSubClass,
comment,
currency,
countriesCount,
dataSource,
id,
isActive,
lastMarketPrice,
name,
symbol,
marketDataItemCount,
sectorsCount,
activitiesCount: _count.activities,
date: activities?.[0]?.date,
isUsedByUsersWithSubscription:
await isUsedByUsersWithSubscription,
watchedByCount: _count.watchedBy
};
}
)
);
if (presetId) {
if (presetId === 'ETF_WITHOUT_COUNTRIES') {
marketData = marketData.filter(({ countriesCount }) => {
return countriesCount === 0;
});
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
marketData = marketData.filter(({ sectorsCount }) => {
return sectorsCount === 0;
});
} }
)
);
count = marketData.length; if (presetId) {
if (presetId === 'ETF_WITHOUT_COUNTRIES') {
marketData = marketData.filter(({ countriesCount }) => {
return countriesCount === 0;
});
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
marketData = marketData.filter(({ sectorsCount }) => {
return sectorsCount === 0;
});
} }
return { count = marketData.length;
count,
marketData
};
} finally {
await extendedPrismaClient.$disconnect();
Logger.debug('Disconnect extended prisma client', 'AdminService');
} }
return {
count,
marketData
};
} }
public async getMarketDataBySymbol({ public async getMarketDataBySymbol({
@ -704,8 +694,6 @@ export class AdminService {
} }
private getExtendedPrismaClient() { private getExtendedPrismaClient() {
Logger.debug('Connect extended prisma client', 'AdminService');
const symbolProfileExtension = Prisma.defineExtension((client) => { const symbolProfileExtension = Prisma.defineExtension((client) => {
return client.$extends({ return client.$extends({
result: { result: {
@ -746,7 +734,7 @@ export class AdminService {
}); });
}); });
return new PrismaClient().$extends(symbolProfileExtension); return this.prismaService.$extends(symbolProfileExtension);
} }
private async getMarketDataForCurrencies(): Promise<AdminMarketData> { private async getMarketDataForCurrencies(): Promise<AdminMarketData> {

3
apps/api/src/app/endpoints/public/public.controller.ts

@ -7,6 +7,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { getSum } from '@ghostfolio/common/helper'; import { getSum } from '@ghostfolio/common/helper';
import { PublicPortfolioResponse } from '@ghostfolio/common/interfaces'; import { PublicPortfolioResponse } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
@ -58,7 +59,7 @@ export class PublicController {
}); });
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = user.subscription.type === 'Premium'; hasDetails = user.subscription.type === SubscriptionType.Premium;
} }
const [ const [

4
apps/api/src/app/endpoints/sitemap/sitemap.service.ts

@ -120,6 +120,10 @@ export class SitemapService {
{ {
languageCode: 'en', languageCode: 'en',
routerLink: ['2025', '11', 'black-weeks-2025'] routerLink: ['2025', '11', 'black-weeks-2025']
},
{
languageCode: 'en',
routerLink: ['2026', '04', 'ghostfolio-3']
} }
] ]
.map(({ languageCode, routerLink }) => { .map(({ languageCode, routerLink }) => {

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

@ -17,6 +17,7 @@ import {
HEADER_KEY_IMPERSONATION, HEADER_KEY_IMPERSONATION,
UNKNOWN_KEY UNKNOWN_KEY
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { import {
PortfolioDetails, PortfolioDetails,
PortfolioDividendsResponse, PortfolioDividendsResponse,
@ -92,7 +93,8 @@ export class PortfolioController {
let hasError = false; let hasError = false;
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = this.request.user.subscription.type === 'Premium'; hasDetails =
this.request.user.subscription.type === SubscriptionType.Premium;
} }
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
@ -356,7 +358,7 @@ export class PortfolioController {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic' this.request.user.subscription.type === SubscriptionType.Basic
) { ) {
dividends = dividends.map((item) => { dividends = dividends.map((item) => {
return nullifyValuesInObject(item, ['investment']); return nullifyValuesInObject(item, ['investment']);
@ -484,7 +486,7 @@ export class PortfolioController {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic' this.request.user.subscription.type === SubscriptionType.Basic
) { ) {
investments = investments.map((item) => { investments = investments.map((item) => {
return nullifyValuesInObject(item, ['investment']); return nullifyValuesInObject(item, ['investment']);
@ -596,7 +598,7 @@ export class PortfolioController {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic' this.request.user.subscription.type === SubscriptionType.Basic
) { ) {
performanceInformation.chart = performanceInformation.chart.map( performanceInformation.chart = performanceInformation.chart.map(
(item) => { (item) => {
@ -624,7 +626,7 @@ export class PortfolioController {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic' this.request.user.subscription.type === SubscriptionType.Basic
) { ) {
for (const category of report.xRay.categories) { for (const category of report.xRay.categories) {
category.rules = null; category.rules = null;

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

@ -32,6 +32,7 @@ import {
TAG_ID_EXCLUDE_FROM_ANALYSIS, TAG_ID_EXCLUDE_FROM_ANALYSIS,
locale as defaultLocale locale as defaultLocale
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { import {
User as IUser, User as IUser,
SystemMessage, SystemMessage,
@ -156,7 +157,7 @@ export class UserService {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
subscription.type === 'Basic' subscription.type === SubscriptionType.Basic
) { ) {
tags = []; tags = [];
} }
@ -443,7 +444,7 @@ export class UserService {
createdAt: user.createdAt createdAt: user.createdAt
}); });
if (user.subscription?.type === 'Basic') { if (user.subscription?.type === SubscriptionType.Basic) {
const daysSinceRegistration = differenceInDays( const daysSinceRegistration = differenceInDays(
new Date(), new Date(),
user.createdAt user.createdAt
@ -485,7 +486,7 @@ export class UserService {
// Reset holdings view mode // Reset holdings view mode
user.settings.settings.holdingsViewMode = undefined; user.settings.settings.holdingsViewMode = undefined;
} else if (user.subscription?.type === 'Premium') { } else if (user.subscription?.type === SubscriptionType.Premium) {
if (!hasRole(user, Role.DEMO)) { if (!hasRole(user, Role.DEMO)) {
currentPermissions.push(permissions.createApiKey); currentPermissions.push(permissions.createApiKey);
currentPermissions.push(permissions.enableDataProviderGhostfolio); currentPermissions.push(permissions.enableDataProviderGhostfolio);

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

@ -40,6 +40,7 @@
"0NE": "Stone", "0NE": "Stone",
"0X0": "0x0.ai", "0X0": "0x0.ai",
"0X1": "0x1.tools: AI Multi-tool Plaform", "0X1": "0x1.tools: AI Multi-tool Plaform",
"0X63SPIKE": "Spike",
"0XBTC": "0xBitcoin", "0XBTC": "0xBitcoin",
"0XCOCO": "0xCoco", "0XCOCO": "0xCoco",
"0XDEV": "DEVAI", "0XDEV": "DEVAI",
@ -773,7 +774,7 @@
"ANALY": "Analysoor", "ANALY": "Analysoor",
"ANARCHISTS": "Anarchists Prime", "ANARCHISTS": "Anarchists Prime",
"ANAT": "Anatolia Token", "ANAT": "Anatolia Token",
"ANB": "Angryb", "ANB": "Ant BlockChain",
"ANC": "Anchor Protocol", "ANC": "Anchor Protocol",
"ANCHOR": "AnchorSwap", "ANCHOR": "AnchorSwap",
"ANCIENTKING": "Ancient Kingdom", "ANCIENTKING": "Ancient Kingdom",
@ -798,6 +799,7 @@
"ANGLE": "ANGLE", "ANGLE": "ANGLE",
"ANGO": "Aureus Nummus Gold", "ANGO": "Aureus Nummus Gold",
"ANGRYSLERF": "ANGRYSLERF", "ANGRYSLERF": "ANGRYSLERF",
"ANGRYTOKEN": "Angryb",
"ANI": "Ani Grok Companion (anicompanion.net)", "ANI": "Ani Grok Companion (anicompanion.net)",
"ANIM": "Animalia", "ANIM": "Animalia",
"ANIMA": "Realm Anima", "ANIMA": "Realm Anima",
@ -1047,6 +1049,7 @@
"ARTDRAW": "ArtDraw", "ARTDRAW": "ArtDraw",
"ARTE": "Artemine", "ARTE": "Artemine",
"ARTEM": "Artem", "ARTEM": "Artem",
"ARTEMIS": "OFFICIAL ARTEMIS",
"ARTEON": "Arteon", "ARTEON": "Arteon",
"ARTEQ": "artèQ", "ARTEQ": "artèQ",
"ARTEX": "Artex", "ARTEX": "Artex",
@ -1120,6 +1123,10 @@
"ASTA": "ASTA", "ASTA": "ASTA",
"ASTER": "Aster", "ASTER": "Aster",
"ASTERINU": "Aster INU", "ASTERINU": "Aster INU",
"ASTEROID": "ASTEROID",
"ASTEROIDBOT": "Asteroid Bot",
"ASTEROIDCOIN": "ASTEROID",
"ASTEROIDETH": "Asteroid",
"ASTHERUSUSDF": "Astherus USDF", "ASTHERUSUSDF": "Astherus USDF",
"ASTO": "Altered State Token", "ASTO": "Altered State Token",
"ASTON": "Aston", "ASTON": "Aston",
@ -1564,6 +1571,7 @@
"BANANO": "Banano", "BANANO": "Banano",
"BANC": "Babes and Nerds", "BANC": "Babes and Nerds",
"BANCA": "BANCA", "BANCA": "BANCA",
"BANCORUSD": "USD Bancor",
"BAND": "Band Protocol", "BAND": "Band Protocol",
"BANDEX": "Banana Index", "BANDEX": "Banana Index",
"BANDIT": "Bandit on Base", "BANDIT": "Bandit on Base",
@ -1602,11 +1610,12 @@
"BART": "BarterTrade", "BART": "BarterTrade",
"BARTKRC": "BART Token", "BARTKRC": "BART Token",
"BARY": "Bary", "BARY": "Bary",
"BAS": "BNB Attestation Service",
"BASEAI": "BaseAI", "BASEAI": "BaseAI",
"BASEBEAR": "BBQ", "BASEBEAR": "BBQ",
"BASECAT": "BASE CAT", "BASECAT": "BASE CAT",
"BASECOIN": "BASECOIN", "BASECOIN": "BASECOIN",
"BASED": "Based Money", "BASED": "Based Token",
"BASEDAI": "BasedAI", "BASEDAI": "BasedAI",
"BASEDALF": "Based Alf", "BASEDALF": "Based Alf",
"BASEDB": "Based Bonk", "BASEDB": "Based Bonk",
@ -1614,12 +1623,13 @@
"BASEDCOPE": "COPE", "BASEDCOPE": "COPE",
"BASEDFINANCE": "Based", "BASEDFINANCE": "Based",
"BASEDHOPPY": "Based Hoppy (basedhoppy.vip)", "BASEDHOPPY": "Based Hoppy (basedhoppy.vip)",
"BASEDMONEY": "Based Money",
"BASEDMONEYV1": "Based Money v1",
"BASEDP": "Based Pepe", "BASEDP": "Based Pepe",
"BASEDR": "Based Rabbit", "BASEDR": "Based Rabbit",
"BASEDS": "BasedSwap", "BASEDS": "BasedSwap",
"BASEDSB": "Based Street Bets", "BASEDSB": "Based Street Bets",
"BASEDTURBO": "Based Turbo", "BASEDTURBO": "Based Turbo",
"BASEDV1": "Based Money v1",
"BASEHEROES": "Baseheroes", "BASEHEROES": "Baseheroes",
"BASEPROTOCOL": "Base Protocol", "BASEPROTOCOL": "Base Protocol",
"BASESWAPX": "BaseX", "BASESWAPX": "BaseX",
@ -1637,6 +1647,7 @@
"BASISSHAREV2": "Basis Share", "BASISSHAREV2": "Basis Share",
"BASK": "BasketDAO", "BASK": "BasketDAO",
"BAST": "Bast", "BAST": "Bast",
"BASTEROID": "BABY ASTEROID",
"BASTET": "Bastet Goddess", "BASTET": "Bastet Goddess",
"BAT": "Basic Attention Token", "BAT": "Basic Attention Token",
"BATCH": "BATCH Token", "BATCH": "BATCH Token",
@ -2584,7 +2595,7 @@
"BOZO": "BOZO", "BOZO": "BOZO",
"BOZOH": "bozo Hybrid", "BOZOH": "bozo Hybrid",
"BOZY": "Book of Crazy", "BOZY": "Book of Crazy",
"BP": "BunnyPark", "BP": "Backpack",
"BPAD": "BlokPad", "BPAD": "BlokPad",
"BPADA": "Binance-Peg Cardano (Binance Bridge)", "BPADA": "Binance-Peg Cardano (Binance Bridge)",
"BPAVAX": "Binance-Peg Avalanche (Binance Bridge)", "BPAVAX": "Binance-Peg Avalanche (Binance Bridge)",
@ -2650,6 +2661,7 @@
"BREAD": "Breadchain Cooperative", "BREAD": "Breadchain Cooperative",
"BREE": "CBDAO", "BREE": "CBDAO",
"BREED": "BreederDAO", "BREED": "BreederDAO",
"BRENT": "Brent Crude",
"BREPE": "BREPE", "BREPE": "BREPE",
"BRETARDIO": "Bretardio", "BRETARDIO": "Bretardio",
"BRETT": "Brett Base", "BRETT": "Brett Base",
@ -2965,6 +2977,7 @@
"BULLA": "BULLA", "BULLA": "BULLA",
"BULLBEAR": "BullBear AI", "BULLBEAR": "BullBear AI",
"BULLC": "BuySell", "BULLC": "BuySell",
"BULLDOG": "BullDog Coin",
"BULLF": "BULL FINANCE", "BULLF": "BULL FINANCE",
"BULLGOD": "Bull God", "BULLGOD": "Bull God",
"BULLI": "Bullish On Ethereum", "BULLI": "Bullish On Ethereum",
@ -2997,6 +3010,7 @@
"BUNNY": "Pancake Bunny", "BUNNY": "Pancake Bunny",
"BUNNYINU": "Bunny Inu", "BUNNYINU": "Bunny Inu",
"BUNNYM": "BUNNY MEV BOT", "BUNNYM": "BUNNY MEV BOT",
"BUNNYP": "BunnyPark",
"BUNNYROCKET": "BunnyRocket", "BUNNYROCKET": "BunnyRocket",
"BURG": "Burger", "BURG": "Burger",
"BURGER": "Burger Swap", "BURGER": "Burger Swap",
@ -3207,7 +3221,8 @@
"CARTIER": "Cartier", "CARTIER": "Cartier",
"CARV": "CARV", "CARV": "CARV",
"CAS": "Cashaa", "CAS": "Cashaa",
"CASH": "CashCoin", "CASH": "CASH",
"CASHCOIN": "CashCoin",
"CASHIO": "Cashio Dollar", "CASHIO": "Cashio Dollar",
"CASHLY": "Cashly", "CASHLY": "Cashly",
"CASHT": "Cash Tech", "CASHT": "Cash Tech",
@ -3232,7 +3247,7 @@
"CATCEO": "CATCEO", "CATCEO": "CATCEO",
"CATCH": "SpaceCatch", "CATCH": "SpaceCatch",
"CATCO": "CatCoin", "CATCO": "CatCoin",
"CATCOIN": "CatCoin", "CATCOINCASH": "CatCoin",
"CATCOINETH": "Catcoin", "CATCOINETH": "Catcoin",
"CATCOINIO": "Catcoin", "CATCOINIO": "Catcoin",
"CATCOINOFSOL": "Cat Coin", "CATCOINOFSOL": "Cat Coin",
@ -3488,7 +3503,7 @@
"CHARTIQ": "ChartIQ", "CHARTIQ": "ChartIQ",
"CHAS": "Chasm", "CHAS": "Chasm",
"CHASH": "CleverHash", "CHASH": "CleverHash",
"CHAT": "Solchat", "CHAT": "OpenChat",
"CHATAI": "ChatAI Token", "CHATAI": "ChatAI Token",
"CHATGPT": "AI Dragon", "CHATGPT": "AI Dragon",
"CHATOSHI": "chAtoshI", "CHATOSHI": "chAtoshI",
@ -3826,7 +3841,8 @@
"COFIX": "CoFIX", "COFIX": "CoFIX",
"COFOUNDIT": "Cofound.it", "COFOUNDIT": "Cofound.it",
"COG": "Cognitio", "COG": "Cognitio",
"COGE": "Cogecoin", "COGE": "Copper Doge",
"COGECOIN": "Cogecoin",
"COGEN": "Cogenero", "COGEN": "Cogenero",
"COGI": "COGI", "COGI": "COGI",
"COGS": "Cogmento", "COGS": "Cogmento",
@ -4350,6 +4366,7 @@
"CWR": "Cowrium", "CWR": "Cowrium",
"CWS": "Crowns", "CWS": "Crowns",
"CWT": "CrossWallet", "CWT": "CrossWallet",
"CWU": "Commonwealth",
"CWV": "CryptoWave", "CWV": "CryptoWave",
"CWX": "Crypto-X", "CWX": "Crypto-X",
"CWXT": "CryptoWorldXToken", "CWXT": "CryptoWorldXToken",
@ -4673,6 +4690,7 @@
"DEFIK": "DeFi Kingdoms JADE", "DEFIK": "DeFi Kingdoms JADE",
"DEFIL": "DeFIL", "DEFIL": "DeFIL",
"DEFILAB": "Defi", "DEFILAB": "Defi",
"DEFINITIVE": "Edge",
"DEFISCALE": "DeFiScale", "DEFISCALE": "DeFiScale",
"DEFISSI": "DEFI.ssi", "DEFISSI": "DEFI.ssi",
"DEFIT": "Digital Fitness", "DEFIT": "Digital Fitness",
@ -5343,7 +5361,8 @@
"DTV": "DraperTV", "DTV": "DraperTV",
"DTX": "DataBroker DAO", "DTX": "DataBroker DAO",
"DUA": "Brillion", "DUA": "Brillion",
"DUAL": "Dual Finance", "DUAL": "DUAL",
"DUALDAOTOKEN": "Dual Finance",
"DUB": "DubCoin", "DUB": "DubCoin",
"DUBAICAT": "Dubai Cat", "DUBAICAT": "Dubai Cat",
"DUBBZ": "Dubbz", "DUBBZ": "Dubbz",
@ -5487,6 +5506,7 @@
"EARTH": "Earth Token", "EARTH": "Earth Token",
"EARTHCOIN": "EarthCoin", "EARTHCOIN": "EarthCoin",
"EARTHM": "Earthmeta", "EARTHM": "Earthmeta",
"EARTHY": "Little Earth Buddy",
"EASY": "EASY", "EASY": "EASY",
"EASYF": "EasyFeedback", "EASYF": "EasyFeedback",
"EASYMINE": "EasyMine", "EASYMINE": "EasyMine",
@ -5566,7 +5586,7 @@
"EDEXA": "edeXa Security Token", "EDEXA": "edeXa Security Token",
"EDFI": "EdFi", "EDFI": "EdFi",
"EDG": "Edgeless", "EDG": "Edgeless",
"EDGE": "Definitive", "EDGE": "edgeX",
"EDGEACTIVITY": "EDGE Activity Token", "EDGEACTIVITY": "EDGE Activity Token",
"EDGEAI": "EdgeAI", "EDGEAI": "EdgeAI",
"EDGEN": "LayerEdge", "EDGEN": "LayerEdge",
@ -5650,6 +5670,7 @@
"EIGENP": "Eigenpie", "EIGENP": "Eigenpie",
"EIM": "Expert Infra", "EIM": "Expert Infra",
"EIQT": "IQ Prediction", "EIQT": "IQ Prediction",
"EITHER": "Eitherway",
"EJAC": "EJA Coin", "EJAC": "EJA Coin",
"EJS": "Enjinstarter", "EJS": "Enjinstarter",
"EKG": "Ekon Gold", "EKG": "Ekon Gold",
@ -5765,6 +5786,7 @@
"EML": "EML Protocol", "EML": "EML Protocol",
"EMMM": "emmm", "EMMM": "emmm",
"EMN.CUR": "Eastman Chemical", "EMN.CUR": "Eastman Chemical",
"EMOGINETWORK": "EMOGI Network",
"EMOJI": "MOMOJI", "EMOJI": "MOMOJI",
"EMON": "Ethermon", "EMON": "Ethermon",
"EMONEYEUR": "e-Money EUR", "EMONEYEUR": "e-Money EUR",
@ -6063,10 +6085,11 @@
"EUSX": "eUSX", "EUSX": "eUSX",
"EUT": "EarnUp Token", "EUT": "EarnUp Token",
"EUTBL": "Spiko EU T-Bills Money Market Fund", "EUTBL": "Spiko EU T-Bills Money Market Fund",
"EV": "EVAI", "EV": "Everything",
"EVA": "Evadore", "EVA": "Evadore",
"EVAA": "EVAA Protocol", "EVAA": "EVAA Protocol",
"EVAI": "EVA Intelligence", "EVAI": "EVA Intelligence",
"EVAIIO": "EVAI",
"EVAL": "Chromia's EVAL by Virtuals", "EVAL": "Chromia's EVAL by Virtuals",
"EVAN": "Evanesco Network", "EVAN": "Evanesco Network",
"EVAULT": "EthereumVault", "EVAULT": "EthereumVault",
@ -7101,6 +7124,7 @@
"GENIESWAP": "GenieSwap", "GENIESWAP": "GenieSwap",
"GENIESWAPV1": "GenieSwap v1", "GENIESWAPV1": "GenieSwap v1",
"GENIFYART": "Genify ART", "GENIFYART": "Genify ART",
"GENIUS": "Genius",
"GENIX": "Genix", "GENIX": "Genix",
"GENO": "GenomeFi", "GENO": "GenomeFi",
"GENOME": "GenomesDao", "GENOME": "GenomesDao",
@ -7740,6 +7764,7 @@
"HACHIK": "Hachiko", "HACHIK": "Hachiko",
"HACHIKO": "Hachiko Inu Token", "HACHIKO": "Hachiko Inu Token",
"HACHIKOINU": "Hachiko Inu", "HACHIKOINU": "Hachiko Inu",
"HACHIKOTOKEN": "Hachiko",
"HACHIONB": "Hachi On Base", "HACHIONB": "Hachi On Base",
"HACHITOKEN": "Hachi", "HACHITOKEN": "Hachi",
"HACK": "HACK", "HACK": "HACK",
@ -7747,6 +7772,7 @@
"HAEDAL": "Haedal Protocol", "HAEDAL": "Haedal Protocol",
"HAGGIS": "New Born Haggis Pygmy Hippo", "HAGGIS": "New Born Haggis Pygmy Hippo",
"HAHA": "Hasaki", "HAHA": "Hasaki",
"HAHAYESRIZO": "Haha Yes Hedgehog",
"HAI": "Hacken Token", "HAI": "Hacken Token",
"HAIO": "HAiO", "HAIO": "HAiO",
"HAIR": " HairDAO", "HAIR": " HairDAO",
@ -7776,6 +7802,7 @@
"HANACOIN": "Hanacoin", "HANACOIN": "Hanacoin",
"HANAETH": "Hana", "HANAETH": "Hana",
"HANAETHCTO": "HANA", "HANAETHCTO": "HANA",
"HANC": "OddHanc",
"HAND": "ShowHand", "HAND": "ShowHand",
"HANDY": "Handy", "HANDY": "Handy",
"HANK": "Hank", "HANK": "Hank",
@ -7806,6 +7833,7 @@
"HASBIK": "Hasbulla", "HASBIK": "Hasbulla",
"HASH": "Provenance Blockchain", "HASH": "Provenance Blockchain",
"HASHAI": "HashAI", "HASHAI": "HashAI",
"HASHCOIN": "HASH Coin",
"HASHNET": "HashNet BitEco", "HASHNET": "HashNet BitEco",
"HASHT": "HASH Token", "HASHT": "HASH Token",
"HASUI": "Haedal", "HASUI": "Haedal",
@ -8411,6 +8439,7 @@
"IG": "IG Token ", "IG": "IG Token ",
"IGCH": "IG-Crypto Holding", "IGCH": "IG-Crypto Holding",
"IGG": "IG Gold", "IGG": "IG Gold",
"IGGT": "The Invincible Game Token",
"IGI": "Igi", "IGI": "Igi",
"IGNIS": "Ignis", "IGNIS": "Ignis",
"IGT": "Infinitar", "IGT": "Infinitar",
@ -8526,7 +8555,8 @@
"INNBC": "Innovative Bioresearch Coin", "INNBC": "Innovative Bioresearch Coin",
"INNOU": "Innou", "INNOU": "Innou",
"INNOVAMINEX": "InnovaMinex", "INNOVAMINEX": "InnovaMinex",
"INO": "Ino Coin", "INO": "InoAi",
"INOCOIN": "Ino Coin",
"INOVAI": "INOVAI", "INOVAI": "INOVAI",
"INP": "Ionic Pocket Token", "INP": "Ionic Pocket Token",
"INRT": "INRToken", "INRT": "INRToken",
@ -8709,6 +8739,7 @@
"IVY": "IvyKoin", "IVY": "IvyKoin",
"IVZ": "InvisibleCoin", "IVZ": "InvisibleCoin",
"IW": "iWallet", "IW": "iWallet",
"IWC": "IWC",
"IWFT": "İstanbul Wild Cats", "IWFT": "İstanbul Wild Cats",
"IWMON": "iShares Russell 2000 ETF (Ondo Tokenized)", "IWMON": "iShares Russell 2000 ETF (Ondo Tokenized)",
"IWT": "IwToken", "IWT": "IwToken",
@ -9382,6 +9413,7 @@
"KRD": "Krypton DAO", "KRD": "Krypton DAO",
"KREDS": "KREDS", "KREDS": "KREDS",
"KREST": "krest Network", "KREST": "krest Network",
"KRGN": "Kerrigan Network",
"KRIDA": "KridaFans", "KRIDA": "KridaFans",
"KRIPTO": "Kripto", "KRIPTO": "Kripto",
"KRL": "Kryll", "KRL": "Kryll",
@ -9897,7 +9929,7 @@
"LOKA": "League of Kingdoms", "LOKA": "League of Kingdoms",
"LOKR": "Polkalokr", "LOKR": "Polkalokr",
"LOKY": "Loky by Virtuals", "LOKY": "Loky by Virtuals",
"LOL": "EMOGI Network", "LOL": "LOL",
"LOLA": "Lola", "LOLA": "Lola",
"LOLATHECAT": "Lola", "LOLATHECAT": "Lola",
"LOLC": "LOL Coin", "LOLC": "LOL Coin",
@ -9907,9 +9939,11 @@
"LOLO": "Lolo", "LOLO": "Lolo",
"LOLONBSC": "LOL", "LOLONBSC": "LOL",
"LON": "Tokenlon", "LON": "Tokenlon",
"LONG": "Longdrink Finance", "LONG": "LONG",
"LONGDRINK": "Longdrink Finance",
"LONGEVITY": "longevity", "LONGEVITY": "longevity",
"LONGFU": "LONGFU", "LONGFU": "LONGFU",
"LONGFUN": "Long",
"LONGM": "Long Mao", "LONGM": "Long Mao",
"LONGSHINE": "LongShine", "LONGSHINE": "LongShine",
"LOOBY": "Looby by Stephen Bliss", "LOOBY": "Looby by Stephen Bliss",
@ -10360,6 +10394,7 @@
"MAXL": "Maxi protocol", "MAXL": "Maxi protocol",
"MAXR": "Max Revive", "MAXR": "Max Revive",
"MAXX": "MAXX Finance", "MAXX": "MAXX Finance",
"MAXXING": "Maxxing",
"MAY": "Mayflower", "MAY": "Mayflower",
"MAYA": "Maya", "MAYA": "Maya",
"MAYACOIN": "MayaCoin", "MAYACOIN": "MayaCoin",
@ -10667,6 +10702,8 @@
"MEXC": "MEXC Token", "MEXC": "MEXC Token",
"MEXP": "MOJI Experience Points", "MEXP": "MOJI Experience Points",
"MEY": "Mey Network", "MEY": "Mey Network",
"MEZO": "MEZO",
"MEZOUSD": "Mezo USD",
"MEZZ": "MEZZ Token", "MEZZ": "MEZZ Token",
"MF": "Moonwalk Fitness", "MF": "Moonwalk Fitness",
"MF1": "Meta Finance", "MF1": "Meta Finance",
@ -10713,7 +10750,8 @@
"MHT": "Mouse Haunt", "MHT": "Mouse Haunt",
"MHUNT": "MetaShooter", "MHUNT": "MetaShooter",
"MI": "XiaoMiCoin", "MI": "XiaoMiCoin",
"MIA": "MiamiCoin", "MIA": "MIA",
"MIAMICOIN": "MiamiCoin",
"MIAO": "MIAOCoin", "MIAO": "MIAOCoin",
"MIB": "Mobile Integrated Blockchain", "MIB": "Mobile Integrated Blockchain",
"MIBO": "miBoodle", "MIBO": "miBoodle",
@ -11034,7 +11072,7 @@
"MOLO": "MOLO CHAIN", "MOLO": "MOLO CHAIN",
"MOLT": "Moltbook", "MOLT": "Moltbook",
"MOLTID": "MoltID", "MOLTID": "MoltID",
"MOM": "Mother of Memes", "MOM": "MOM",
"MOMA": "Mochi Market", "MOMA": "Mochi Market",
"MOMIJI": "MAGA Momiji", "MOMIJI": "MAGA Momiji",
"MOMO": "Momo", "MOMO": "Momo",
@ -11120,6 +11158,7 @@
"MOONEY": "Moon DAO", "MOONEY": "Moon DAO",
"MOONI": "MOON INU", "MOONI": "MOON INU",
"MOONION": "Moonions", "MOONION": "Moonions",
"MOONKIN": "MOONKIN",
"MOONKIZE": "MoonKize", "MOONKIZE": "MoonKize",
"MOONLIGHT": "Moonlight Token", "MOONLIGHT": "Moonlight Token",
"MOONPIG": "Moonpig", "MOONPIG": "Moonpig",
@ -11156,6 +11195,7 @@
"MOTG": "MetaOctagon", "MOTG": "MetaOctagon",
"MOTH": "MOTH", "MOTH": "MOTH",
"MOTHER": "Mother Iggy", "MOTHER": "Mother Iggy",
"MOTHEROFMEMES": "Mother of Memes",
"MOTI": "Motion", "MOTI": "Motion",
"MOTION": "motion", "MOTION": "motion",
"MOTIONCOIN": "Motion", "MOTIONCOIN": "Motion",
@ -12140,7 +12180,7 @@
"OEX": "OEX", "OEX": "OEX",
"OF": "OFCOIN", "OF": "OFCOIN",
"OFBC": "OneFinBank Coin", "OFBC": "OneFinBank Coin",
"OFC": "$OFC Coin", "OFC": "OneFootball Club",
"OFCR": "CryptoPolice", "OFCR": "CryptoPolice",
"OFE": "Ofero", "OFE": "Ofero",
"OFF": "BlastOff", "OFF": "BlastOff",
@ -12278,6 +12318,7 @@
"ONLINE": "Onlinebase", "ONLINE": "Onlinebase",
"ONLY": "OnlyCam", "ONLY": "OnlyCam",
"ONLYCUMIES": "OnlyCumies", "ONLYCUMIES": "OnlyCumies",
"ONLYFANSCOINS": "$OFC Coin",
"ONNO": "Onno Vault", "ONNO": "Onno Vault",
"ONOMY": "Onomy Protocol", "ONOMY": "Onomy Protocol",
"ONOT": "ONO", "ONOT": "ONO",
@ -12506,6 +12547,7 @@
"OXY2": "Cryptoxygen", "OXY2": "Cryptoxygen",
"OXYC": "Oxycoin", "OXYC": "Oxycoin",
"OYS": "Oyster Platform", "OYS": "Oyster Platform",
"OYSTERPEARL": "Oyster Pearl",
"OZG": "Ozagold", "OZG": "Ozagold",
"OZK": "OrdiZK", "OZK": "OrdiZK",
"OZMPC": "Ozempic", "OZMPC": "Ozempic",
@ -13021,7 +13063,6 @@
"PINMO": "Pinmo", "PINMO": "Pinmo",
"PINO": "Pinocchu", "PINO": "Pinocchu",
"PINS": "PINs Network Token", "PINS": "PINs Network Token",
"PINU": "Piccolo Inu",
"PINU100X": "Pi INU 100x", "PINU100X": "Pi INU 100x",
"PIO": "Pioneershares", "PIO": "Pioneershares",
"PIP": "Pip", "PIP": "Pip",
@ -13412,7 +13453,7 @@
"PRISMA": "Prisma Finance", "PRISMA": "Prisma Finance",
"PRIVIX": "Privix", "PRIVIX": "Privix",
"PRIX": "Privatix", "PRIX": "Privatix",
"PRL": "Oyster Pearl", "PRL": "Perle",
"PRM": "PrismChain", "PRM": "PrismChain",
"PRMX": "PREMA", "PRMX": "PREMA",
"PRNT": "Prime Numbers", "PRNT": "Prime Numbers",
@ -13442,6 +13483,7 @@
"PROTEO": "Proteo DeFi", "PROTEO": "Proteo DeFi",
"PROTO": "Protocon", "PROTO": "Protocon",
"PROTOCOLZ": "Protocol Zero", "PROTOCOLZ": "Protocol Zero",
"PROTOKEN": "Pro Token",
"PROTON": "Proton", "PROTON": "Proton",
"PROUD": "PROUD Money", "PROUD": "PROUD Money",
"PROVE": "Succinct", "PROVE": "Succinct",
@ -13540,6 +13582,7 @@
"PUMPB": "Pump", "PUMPB": "Pump",
"PUMPBTC": "pumpBTC", "PUMPBTC": "pumpBTC",
"PUMPBTCXYZ": "PumpBTC", "PUMPBTCXYZ": "PumpBTC",
"PUMPCADE": "PUMPCADE",
"PUMPFUNBAN": "Pump Fun Ban", "PUMPFUNBAN": "Pump Fun Ban",
"PUMPIT": "BOGDANOFF", "PUMPIT": "BOGDANOFF",
"PUMPTRUMP": "PUMP TRUMP", "PUMPTRUMP": "PUMP TRUMP",
@ -14058,6 +14101,7 @@
"RETSA": "Retsa Coin", "RETSA": "Retsa Coin",
"REU": "REUCOIN", "REU": "REUCOIN",
"REUNI": "Reunit Wallet", "REUNI": "Reunit Wallet",
"REUR": "Royal Euro",
"REUSDC": "Relend USDC", "REUSDC": "Relend USDC",
"REV": "Revain", "REV": "Revain",
"REV3L": "REV3AL", "REV3L": "REV3AL",
@ -14159,6 +14203,7 @@
"RIPTO": "RiptoBuX", "RIPTO": "RiptoBuX",
"RIS": "Riser", "RIS": "Riser",
"RISE": "EverRise", "RISE": "EverRise",
"RISECOIN": "Rise coin",
"RISEP": "Rise Protocol", "RISEP": "Rise Protocol",
"RISEVISION": "Rise", "RISEVISION": "Rise",
"RISITA": "Risitas", "RISITA": "Risitas",
@ -14166,6 +14211,7 @@
"RITE": "ritestream", "RITE": "ritestream",
"RITO": "Ritocoin", "RITO": "Ritocoin",
"RITZ": "Ritz.Game", "RITZ": "Ritz.Game",
"RIV": "RIV Coin",
"RIVER": "River", "RIVER": "River",
"RIVERPTS": "River Point Reward Token", "RIVERPTS": "River Point Reward Token",
"RIVUS": "RivusDAO", "RIVUS": "RivusDAO",
@ -14602,7 +14648,7 @@
"SBCH": "Smart Bitcoin Cash", "SBCH": "Smart Bitcoin Cash",
"SBE": "Sombe", "SBE": "Sombe",
"SBEFE": "BEFE", "SBEFE": "BEFE",
"SBET": "SBET", "SBET": "Sports Bet",
"SBF": "SBF In Jail", "SBF": "SBF In Jail",
"SBGO": "Bingo Share", "SBGO": "Bingo Share",
"SBIO": "Vector Space Biosciences, Inc.", "SBIO": "Vector Space Biosciences, Inc.",
@ -15410,6 +15456,7 @@
"SOLCAT": "CatSolHat", "SOLCAT": "CatSolHat",
"SOLCATMEME": "SOLCAT", "SOLCATMEME": "SOLCAT",
"SOLCEX": "SolCex", "SOLCEX": "SolCex",
"SOLCHAT": "Solchat",
"SOLCHICKSSHARDS": "SolChicks Shards", "SOLCHICKSSHARDS": "SolChicks Shards",
"SOLE": "SoleCoin", "SOLE": "SoleCoin",
"SOLER": "Solerium", "SOLER": "Solerium",
@ -15420,6 +15467,7 @@
"SOLFUN": "SolFun", "SOLFUN": "SolFun",
"SOLGOAT": "SOLGOAT", "SOLGOAT": "SOLGOAT",
"SOLGUN": "Solgun", "SOLGUN": "Solgun",
"SOLIB": "Solitaire Blossom",
"SOLIC": "Solice", "SOLIC": "Solice",
"SOLID": "Solidified", "SOLID": "Solidified",
"SOLIDSEX": "SOLIDsex: Tokenized veSOLID", "SOLIDSEX": "SOLIDsex: Tokenized veSOLID",
@ -15583,7 +15631,11 @@
"SPIDER": "Spider Man", "SPIDER": "Spider Man",
"SPIDERMAN": "SPIDERMAN BITCOIN", "SPIDERMAN": "SPIDERMAN BITCOIN",
"SPIDEY": "Spidey", "SPIDEY": "Spidey",
"SPIK": "Spike",
"SPIKE": "Spiking", "SPIKE": "Spiking",
"SPIKE1984": "Spike 1984",
"SPIKECOIN": "SPIKE",
"SPIKEFURIE": "SPIKE",
"SPILLWAYS": "SpillWays", "SPILLWAYS": "SpillWays",
"SPIN": "SPIN Protocol", "SPIN": "SPIN Protocol",
"SPINT": "Spintria", "SPINT": "Spintria",
@ -15591,6 +15643,7 @@
"SPITT": "Hawk Ttuuaahh", "SPITT": "Hawk Ttuuaahh",
"SPIZ": "SPACE-iZ", "SPIZ": "SPACE-iZ",
"SPK": "Spark", "SPK": "Spark",
"SPKI": "SPIKE INU",
"SPKL": "SpokLottery", "SPKL": "SpokLottery",
"SPKTR": "Ghost Coin", "SPKTR": "Ghost Coin",
"SPKY": "GhostyCash", "SPKY": "GhostyCash",
@ -15612,6 +15665,7 @@
"SPOOL": "Spool DAO Token", "SPOOL": "Spool DAO Token",
"SPORE": "Spore", "SPORE": "Spore",
"SPORT": "SportsCoin", "SPORT": "SportsCoin",
"SPORTBET": "SBET",
"SPORTFUN": "Sport.fun", "SPORTFUN": "Sport.fun",
"SPORTS": "ZenSports", "SPORTS": "ZenSports",
"SPORTSFIX": "SportsFix", "SPORTSFIX": "SportsFix",
@ -16571,6 +16625,7 @@
"TKX": "Tokenize Xchange", "TKX": "Tokenize Xchange",
"TKY": "THEKEY Token", "TKY": "THEKEY Token",
"TLC": "Trillioner", "TLC": "Trillioner",
"TLF": "Tradeleaf",
"TLM": "Alien Worlds", "TLM": "Alien Worlds",
"TLN": "Trustlines Network", "TLN": "Trustlines Network",
"TLOS": "Telos", "TLOS": "Telos",
@ -17075,6 +17130,7 @@
"UBQ": "Ubiq", "UBQ": "Ubiq",
"UBT": "UniBright", "UBT": "UniBright",
"UBTC": "UnitedBitcoin", "UBTC": "UnitedBitcoin",
"UBU": "UBU",
"UBX": "UBIX Network", "UBX": "UBIX Network",
"UBXN": "UpBots Token", "UBXN": "UpBots Token",
"UBXS": "UBXS", "UBXS": "UBXS",
@ -17169,6 +17225,7 @@
"UNBNK": "Unbanked", "UNBNK": "Unbanked",
"UNBREAKABLE": "UnbreakableCoin", "UNBREAKABLE": "UnbreakableCoin",
"UNC": "UnCoin", "UNC": "UnCoin",
"UNCEROID": "unc asteroid",
"UNCL": "UNCL", "UNCL": "UNCL",
"UNCN": "Unseen", "UNCN": "Unseen",
"UNCOMMONGOODS": "UNCOMMON•GOODS", "UNCOMMONGOODS": "UNCOMMON•GOODS",
@ -17241,6 +17298,7 @@
"UNRC": "UniversalRoyalCoin", "UNRC": "UniversalRoyalCoin",
"UNS": "UNS TOKEN", "UNS": "UNS TOKEN",
"UNSHETH": "unshETH Ether", "UNSHETH": "unshETH Ether",
"UNT": "Uni Token",
"UNW": "UniWorld", "UNW": "UniWorld",
"UOP": "Utopia Genesis Foundation", "UOP": "Utopia Genesis Foundation",
"UOS": "UOS", "UOS": "UOS",
@ -17296,7 +17354,7 @@
"USDAI": "USDai", "USDAI": "USDai",
"USDAP": "Bond Appetite USD", "USDAP": "Bond Appetite USD",
"USDAVALON": "USDa", "USDAVALON": "USDa",
"USDB": "USD Bancor", "USDB": "Blynex USD",
"USDBC": "Bridged USDC", "USDBC": "Bridged USDC",
"USDBLAST": "USDB Blast", "USDBLAST": "USDB Blast",
"USDC": "USD Coin", "USDC": "USD Coin",
@ -17412,13 +17470,14 @@
"UTNP": "Universa", "UTNP": "Universa",
"UTON": "uTON", "UTON": "uTON",
"UTOPIA": "Utopia", "UTOPIA": "Utopia",
"UTOPIAUSD": "Utopia USD",
"UTT": "uTrade", "UTT": "uTrade",
"UTU": "UTU Protocol", "UTU": "UTU Protocol",
"UTX": "UTIX", "UTX": "UTIX",
"UTYA": "Utya", "UTYA": "Utya",
"UTYAB": "Utya Black", "UTYAB": "Utya Black",
"UUC": "USA Unity Coin", "UUC": "USA Unity Coin",
"UUSD": "Utopia USD", "UUSD": "Unity USD",
"UUU": "U Network", "UUU": "U Network",
"UVT": "UvToken", "UVT": "UvToken",
"UW3S": "Utility Web3Shot", "UW3S": "Utility Web3Shot",
@ -17621,7 +17680,8 @@
"VIKKY": "VikkyToken", "VIKKY": "VikkyToken",
"VILADY": "Vitalik Milady", "VILADY": "Vitalik Milady",
"VIM": "VicMove", "VIM": "VicMove",
"VIN": "VinChain", "VIN": "VulgarTycoon",
"VINCHAIN": "VinChain",
"VINCI": "VINCI", "VINCI": "VINCI",
"VINE": "Vine Coin", "VINE": "Vine Coin",
"VINU": "Vita Inu", "VINU": "Vita Inu",
@ -17716,6 +17776,7 @@
"VOL": "Volume Network", "VOL": "Volume Network",
"VOLBOOST": "VolBoost", "VOLBOOST": "VolBoost",
"VOLLAR": "Vollar", "VOLLAR": "Vollar",
"VOLM": "VOLM",
"VOLR": "Volare Network", "VOLR": "Volare Network",
"VOLT": "Volt Inu", "VOLT": "Volt Inu",
"VOLTA": "Volta Club", "VOLTA": "Volta Club",
@ -18488,6 +18549,7 @@
"XCG": "Xchange", "XCG": "Xchange",
"XCH": "Chia", "XCH": "Chia",
"XCHAT": "XChat", "XCHAT": "XChat",
"XCHATSOL": "XChat",
"XCHF": "CryptoFranc", "XCHF": "CryptoFranc",
"XCHNG": "Chainge Finance", "XCHNG": "Chainge Finance",
"XCI": "Cannabis Industry Coin", "XCI": "Cannabis Industry Coin",
@ -18543,6 +18605,7 @@
"XENOVERSE": "Xenoverse", "XENOVERSE": "Xenoverse",
"XEP": "Electra Protocol", "XEP": "Electra Protocol",
"XERA": "XERA", "XERA": "XERA",
"XERO": "XERO",
"XERS": "X Project", "XERS": "X Project",
"XES": "Proxeus", "XES": "Proxeus",
"XET": "Xfinite Entertainment Token", "XET": "Xfinite Entertainment Token",

4
apps/api/src/middlewares/html-template.middleware.ts

@ -83,6 +83,10 @@ const locales = {
'/en/blog/2025/11/black-weeks-2025': { '/en/blog/2025/11/black-weeks-2025': {
featureGraphicPath: 'assets/images/blog/black-weeks-2025.jpg', featureGraphicPath: 'assets/images/blog/black-weeks-2025.jpg',
title: `Black Weeks 2025 - ${title}` title: `Black Weeks 2025 - ${title}`
},
'/en/blog/2026/04/ghostfolio-3': {
featureGraphicPath: 'assets/images/blog/ghostfolio-3.jpg',
title: `Announcing Ghostfolio 3.0 - ${title}`
} }
}; };

7
apps/api/src/services/data-provider/data-provider.service.ts

@ -12,6 +12,7 @@ import {
PROPERTY_DATA_SOURCE_MAPPING PROPERTY_DATA_SOURCE_MAPPING
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { CreateOrderDto } from '@ghostfolio/common/dtos'; import { CreateOrderDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { import {
DATE_FORMAT, DATE_FORMAT,
getAssetProfileIdentifier, getAssetProfileIdentifier,
@ -227,7 +228,7 @@ export class DataProviderService implements OnModuleInit {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user.subscription.type === 'Basic' user.subscription.type === SubscriptionType.Basic
) { ) {
const dataProvider = this.getDataProvider(DataSource[dataSource]); const dataProvider = this.getDataProvider(DataSource[dataSource]);
@ -591,7 +592,7 @@ export class DataProviderService implements OnModuleInit {
} else if ( } else if (
dataProvider.getDataProviderInfo().isPremium && dataProvider.getDataProviderInfo().isPremium &&
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user?.subscription.type === 'Basic' user?.subscription.type === SubscriptionType.Basic
) { ) {
// Skip symbols of Premium data providers for users without subscription // Skip symbols of Premium data providers for users without subscription
return false; return false;
@ -780,7 +781,7 @@ export class DataProviderService implements OnModuleInit {
}) })
.map((lookupItem) => { .map((lookupItem) => {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
if (user.subscription.type === 'Premium') { if (user.subscription.type === SubscriptionType.Premium) {
lookupItem.dataProviderInfo.isPremium = false; lookupItem.dataProviderInfo.isPremium = false;
} }

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

@ -8,7 +8,10 @@ import {
PROPERTY_SYSTEM_MESSAGE, PROPERTY_SYSTEM_MESSAGE,
ghostfolioPrefix ghostfolioPrefix
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import {
ConfirmationDialogType,
SubscriptionType
} from '@ghostfolio/common/enums';
import { getDateFnsLocale } from '@ghostfolio/common/helper'; import { getDateFnsLocale } from '@ghostfolio/common/helper';
import { import {
Coupon, Coupon,
@ -255,7 +258,7 @@ export class GfAdminOverviewComponent implements OnInit {
this.systemMessage ?? this.systemMessage ??
({ ({
message: '⚒️ Scheduled maintenance in progress...', message: '⚒️ Scheduled maintenance in progress...',
targetGroups: ['Basic', 'Premium'] targetGroups: [SubscriptionType.Basic, SubscriptionType.Premium]
} as SystemMessage) } as SystemMessage)
) )
); );

21
apps/client/src/app/pages/blog/2026/04/ghostfolio-3/ghostfolio-3-page.component.ts

@ -0,0 +1,21 @@
import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@Component({
host: { class: 'page' },
imports: [MatButtonModule, RouterModule],
selector: 'gf-ghostfolio-3-page',
templateUrl: './ghostfolio-3-page.html'
})
export class Ghostfolio3PageComponent {
public pricingUrl = `https://ghostfol.io/${document.documentElement.lang}/${publicRoutes.pricing.path}`;
public routerLinkAbout = publicRoutes.about.routerLink;
public routerLinkAboutChangelog =
publicRoutes.about.subRoutes.changelog.routerLink;
public routerLinkBlog = publicRoutes.blog.routerLink;
public routerLinkFeatures = publicRoutes.features.routerLink;
public routerLinkMarkets = publicRoutes.markets.routerLink;
}

281
apps/client/src/app/pages/blog/2026/04/ghostfolio-3/ghostfolio-3-page.html

@ -0,0 +1,281 @@
<div class="blog container">
<div class="row">
<div class="col-md-8 offset-md-2">
<article>
<div class="mb-4 text-center">
<h1 class="mb-1">Ghostfolio 3.0</h1>
<div class="mb-3 text-muted"><small>2026-04-22</small></div>
<img
alt="Ghostfolio 3.0 Teaser"
class="border rounded w-100"
src="../assets/images/blog/ghostfolio-3.jpg"
title="Announcing Ghostfolio 3.0"
/>
</div>
<section class="mb-4">
<p>
Since the
<a href="../en/blog/2023/09/ghostfolio-2">last major version</a>
of <a [routerLink]="routerLinkAbout">Ghostfolio</a>, we have shipped
over 250 releases. The project now counts 275+ contributors from
around the globe and has surpassed 2’300’000 pulls on Docker Hub.
These milestones reflect steady adoption and our focus on
simplifying investment tracking while prioritizing user privacy.
</p>
<p>
Today’s release marks the next major version in our Open Source
Software (OSS) journey.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Introducing Ghostfolio 3.0</h2>
<p>
Ghostfolio 3.0 is the evolution of our
<a [routerLink]="routerLinkFeatures"
>open source wealth management software</a
>, with meaningful improvements for both users and developers. We
have refreshed the user interface, expanded analytics, improved
stability, added more languages, and updated the technology stack to
support these changes. Here is a closer look at a selection of what
you can expect from this
<a [routerLink]="routerLinkAboutChangelog">release</a>, alongside
many smaller additions and enhancements.
</p>
<h3 class="h5">Refreshed User Interface</h3>
<p>
Ghostfolio 3.0 comes with a refreshed user interface that modernizes
the visual appearance of the application. The updated design is
cleaner, with refined components and improved consistency across the
platform.
</p>
<h3 class="h5">Comprehensive Analytics</h3>
<p>
This release provides a broader set of tools to help you understand
your portfolio. Ghostfolio X-ray uses static analysis to highlight
potential issues and risks, with rules that can now be customized to
match your investment strategy.
</p>
<h3 class="h5">Extended Multilanguage Support</h3>
<p>
Largely driven by contributions from the community, Ghostfolio now
supports more languages than ever. The application is now available
in a growing number of languages, making it accessible to a broader
audience of investors around the world.
</p>
<h3 class="h5">Reliable Stability</h3>
<p>
A wealth management platform needs to be reliable. With Ghostfolio
3.0, we have further strengthened the robustness of our
architecture, so you can rely on Ghostfolio across different
<a [routerLink]="routerLinkMarkets">market conditions</a>.
</p>
<h3 class="h5">Empowering Self-Hosting</h3>
<p>
This release simplifies and extends the self-hosting experience. A
major addition is that self-hosters can now fully benefit from
<a target="_blank" [href]="pricingUrl">Ghostfolio Premium</a> to
make use of a professional data provider. This gives you full
control over your infrastructure while still giving you access to
high-quality market data for portfolio analytics.
</p>
<h3 class="h5">Updated Technology Stack</h3>
<p>
Under the hood, Ghostfolio 3.0 has been upgraded to
<a href="https://angular.io" target="_blank">Angular 21</a>,
<a href="https://nestjs.com" target="_blank">Nest.js 11</a>,
<a href="https://www.prisma.io" target="_blank">Prisma 7</a>, and
<a href="https://nx.dev" target="_blank">Nx 22</a>. Keeping the
technology stack up to date helps us provide a solid foundation for
users and developers.
</p>
</section>
<section class="mb-4">
<h2 class="h4">Thriving Ghostfolio Community</h2>
<p>
Ghostfolio is built in public, and its community plays a central
role in shaping the open source project. Here are some highlights of
the community growth:
</p>
<ul>
<li>
Ghostfolio has accumulated <strong>8’000+ stars</strong> on
<a
href="https://github.com/ghostfolio/ghostfolio"
target="_blank"
title="Find Ghostfolio on GitHub"
>GitHub</a
>, reflecting the growing interest and trust in the project.
</li>
<li>
The
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
target="_blank"
title="Join the Ghostfolio Slack community"
>Slack</a
>
community has grown to over <strong>1’250 members</strong>, where
investors exchange ideas and help each other.
</li>
<li>
Over
<strong>700 investors and personal finance enthusiasts</strong>
follow Ghostfolio on
<a href="https://x.com/ghostfolio_" target="_blank">X</a>
(formerly Twitter) for updates and discussions.
</li>
</ul>
<p>
There is much more to come. If you are not part of the community
yet, we would love to have you on board.
</p>
<p>
<strong>Join our Slack community</strong>: Connect with fellow
investors, share insights, and stay updated by joining our
<a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
target="_blank"
title="Join the Ghostfolio Slack community"
>Slack</a
>
community.
</p>
<p>
<strong>Follow us on X</strong>: For release updates and market
insights, follow
<a href="https://x.com/ghostfolio_" target="_blank"
>Ghostfolio on X</a
>
to stay informed.
</p>
<p>
<strong>Give us a Star</strong>: If Ghostfolio has been useful to
you, please consider giving us a star on
<a
href="https://github.com/ghostfolio/ghostfolio"
target="_blank"
title="Find Ghostfolio on GitHub"
>GitHub</a
>. Your support helps us continue improving Ghostfolio.
</p>
<p>
<strong>Become a contributor</strong>: Interested in getting
involved? We welcome contributions from developers who are
passionate about open source and personal finance.
<a href="https://github.com/ghostfolio/ghostfolio" target="_blank"
>Join our developer community</a
>
and help shape the future of Ghostfolio.
</p>
</section>
<section>
<p>
Ghostfolio 3.0 is the result of countless contributions, feedback,
and shared passion for open source and personal finance. Whether you
have been with us from the start or are just discovering the
project, thank you for being part of this community.
</p>
<p>Thomas from Ghostfolio</p>
</section>
<section class="mb-4">
<ul class="list-inline">
<li class="list-inline-item">
<span class="badge badge-light">Angular</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Announcement</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Collaboration</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Community</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Contribution</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Evolution</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Fintech</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Ghostfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Ghostfolio 3.0</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Ghostfolio Premium</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Internationalization</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Investment</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Nest.js</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Nx</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Open Source</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">OSS</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Personal Finance</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Platform</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Portfolio</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Prisma</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Privacy</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Release</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Self-Hosting</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Software</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Stack</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Technology</span>
</li>
<li class="list-inline-item">
<span class="badge badge-light">Wealth Management</span>
</li>
</ul>
</section>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a i18n [routerLink]="routerLinkBlog">Blog</a>
</li>
<li
aria-current="page"
class="active breadcrumb-item text-truncate"
>
Announcing Ghostfolio 3.0
</li>
</ol>
</nav>
</article>
</div>
</div>
</div>

26
apps/client/src/app/pages/blog/blog-page.html

@ -8,6 +8,32 @@
finance</small finance</small
> >
</h1> </h1>
<mat-card appearance="outlined" class="mb-3">
<mat-card-content class="p-0">
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex overflow-hidden p-3 w-100"
href="../en/blog/2026/04/ghostfolio-3"
>
<div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate">
Announcing Ghostfolio 3.0
</div>
<div class="d-flex text-muted">2026-04-03</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
/>
</div>
</a>
</div>
</div>
</mat-card-content>
</mat-card>
@if (hasPermissionForSubscription) { @if (hasPermissionForSubscription) {
<mat-card appearance="outlined" class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-content class="p-0"> <mat-card-content class="p-0">

9
apps/client/src/app/pages/blog/blog-page.routes.ts

@ -218,5 +218,14 @@ export const routes: Routes = [
(c) => c.BlackWeeks2025PageComponent (c) => c.BlackWeeks2025PageComponent
), ),
title: 'Black Weeks 2025' title: 'Black Weeks 2025'
},
{
canActivate: [AuthGuard],
path: '2026/04/ghostfolio-3',
loadComponent: () =>
import('./2026/04/ghostfolio-3/ghostfolio-3-page.component').then(
(c) => c.Ghostfolio3PageComponent
),
title: 'Ghostfolio 3.0'
} }
]; ];

5
apps/client/src/app/pages/portfolio/fire/fire-page.component.ts

@ -1,5 +1,6 @@
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 { SubscriptionType } from '@ghostfolio/common/enums';
import { import {
FireCalculationCompleteEvent, FireCalculationCompleteEvent,
FireWealth, FireWealth,
@ -80,7 +81,7 @@ export class GfFirePageComponent implements OnInit {
: 0 : 0
} }
}; };
if (this.user.subscription?.type === 'Basic') { if (this.user.subscription?.type === SubscriptionType.Basic) {
this.fireWealth = { this.fireWealth = {
today: { today: {
valueInBaseCurrency: 10000 valueInBaseCurrency: 10000
@ -113,7 +114,7 @@ export class GfFirePageComponent implements OnInit {
this.user = state.user; this.user = state.user;
this.hasPermissionToUpdateUserSettings = this.hasPermissionToUpdateUserSettings =
this.user.subscription?.type === 'Basic' this.user.subscription?.type === SubscriptionType.Basic
? false ? false
: hasPermission( : hasPermission(
this.user.permissions, this.user.permissions,

3
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts

@ -2,6 +2,7 @@ import { GfRulesComponent } from '@ghostfolio/client/components/rules/rules.comp
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 { UpdateUserSettingDto } from '@ghostfolio/common/dtos'; import { UpdateUserSettingDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { import {
PortfolioReportResponse, PortfolioReportResponse,
PortfolioReportRule PortfolioReportRule
@ -73,7 +74,7 @@ export class GfXRayPageComponent {
this.user = state.user; this.user = state.user;
this.hasPermissionToUpdateUserSettings = this.hasPermissionToUpdateUserSettings =
this.user.subscription?.type === 'Basic' this.user.subscription?.type === SubscriptionType.Basic
? false ? false
: hasPermission( : hasPermission(
this.user.permissions, this.user.permissions,

BIN
apps/client/src/assets/images/blog/ghostfolio-3.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

7
apps/client/src/assets/oss-friends.json

@ -1,5 +1,5 @@
{ {
"createdAt": "2025-12-08T00:00:00.000Z", "createdAt": "2026-04-21T00:00:00.000Z",
"data": [ "data": [
{ {
"name": "Activepieces", "name": "Activepieces",
@ -21,11 +21,6 @@
"description": "Fastest LLM gateway with adaptive load balancer, cluster mode, guardrails, 1000+ models support & <100 µs overhead at 5k RPS.", "description": "Fastest LLM gateway with adaptive load balancer, cluster mode, guardrails, 1000+ models support & <100 µs overhead at 5k RPS.",
"href": "https://www.getmaxim.ai/bifrost" "href": "https://www.getmaxim.ai/bifrost"
}, },
{
"name": "Cal.com",
"description": "Cal.com is a scheduling tool that helps you schedule meetings without the back-and-forth emails.",
"href": "https://cal.com"
},
{ {
"name": "Cap", "name": "Cap",
"description": "Cap is the open source alternative to Loom. Lightweight, powerful, and cross-platform. Record and share securely in seconds.", "description": "Cap is the open source alternative to Loom. Lightweight, powerful, and cross-platform. Record and share securely in seconds.",

3698
package-lock.json

File diff suppressed because it is too large

42
package.json

@ -76,15 +76,15 @@
"@keyv/redis": "4.4.0", "@keyv/redis": "4.4.0",
"@nestjs/bull": "11.0.4", "@nestjs/bull": "11.0.4",
"@nestjs/cache-manager": "3.1.0", "@nestjs/cache-manager": "3.1.0",
"@nestjs/common": "11.1.14", "@nestjs/common": "11.1.19",
"@nestjs/config": "4.0.3", "@nestjs/config": "4.0.4",
"@nestjs/core": "11.1.14", "@nestjs/core": "11.1.19",
"@nestjs/event-emitter": "3.0.1", "@nestjs/event-emitter": "3.0.1",
"@nestjs/jwt": "11.0.2", "@nestjs/jwt": "11.0.2",
"@nestjs/passport": "11.0.5", "@nestjs/passport": "11.0.5",
"@nestjs/platform-express": "11.1.14", "@nestjs/platform-express": "11.1.19",
"@nestjs/schedule": "6.1.1", "@nestjs/schedule": "6.1.3",
"@nestjs/serve-static": "5.0.4", "@nestjs/serve-static": "5.0.5",
"@openrouter/ai-sdk-provider": "0.7.2", "@openrouter/ai-sdk-provider": "0.7.2",
"@prisma/adapter-pg": "7.7.0", "@prisma/adapter-pg": "7.7.0",
"@prisma/client": "7.7.0", "@prisma/client": "7.7.0",
@ -107,7 +107,7 @@
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"countries-and-timezones": "3.8.0", "countries-and-timezones": "3.8.0",
"countries-list": "3.3.0", "countries-list": "3.3.0",
"countup.js": "2.9.0", "countup.js": "2.10.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"dotenv": "17.2.3", "dotenv": "17.2.3",
"dotenv-expand": "12.0.3", "dotenv-expand": "12.0.3",
@ -118,7 +118,7 @@
"helmet": "7.0.0", "helmet": "7.0.0",
"http-status-codes": "2.3.0", "http-status-codes": "2.3.0",
"ionicons": "8.0.13", "ionicons": "8.0.13",
"jsonpath": "1.2.1", "jsonpath": "1.3.0",
"lodash": "4.18.1", "lodash": "4.18.1",
"marked": "17.0.2", "marked": "17.0.2",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
@ -156,18 +156,18 @@
"@angular/pwa": "21.2.6", "@angular/pwa": "21.2.6",
"@eslint/eslintrc": "3.3.1", "@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.35.0", "@eslint/js": "9.35.0",
"@nestjs/schematics": "11.0.9", "@nestjs/schematics": "11.1.0",
"@nestjs/testing": "11.1.14", "@nestjs/testing": "11.1.19",
"@nx/angular": "22.6.4", "@nx/angular": "22.6.5",
"@nx/eslint-plugin": "22.6.4", "@nx/eslint-plugin": "22.6.5",
"@nx/jest": "22.6.4", "@nx/jest": "22.6.5",
"@nx/js": "22.6.4", "@nx/js": "22.6.5",
"@nx/module-federation": "22.6.4", "@nx/module-federation": "22.6.5",
"@nx/nest": "22.6.4", "@nx/nest": "22.6.5",
"@nx/node": "22.6.4", "@nx/node": "22.6.5",
"@nx/storybook": "22.6.4", "@nx/storybook": "22.6.5",
"@nx/web": "22.6.4", "@nx/web": "22.6.5",
"@nx/workspace": "22.6.4", "@nx/workspace": "22.6.5",
"@schematics/angular": "21.2.6", "@schematics/angular": "21.2.6",
"@storybook/addon-docs": "10.1.10", "@storybook/addon-docs": "10.1.10",
"@storybook/angular": "10.1.10", "@storybook/angular": "10.1.10",
@ -193,7 +193,7 @@
"jest": "30.2.0", "jest": "30.2.0",
"jest-environment-jsdom": "30.2.0", "jest-environment-jsdom": "30.2.0",
"jest-preset-angular": "16.0.0", "jest-preset-angular": "16.0.0",
"nx": "22.6.4", "nx": "22.6.5",
"prettier": "3.8.2", "prettier": "3.8.2",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.7.0", "prisma": "7.7.0",

Loading…
Cancel
Save