diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7936c1cf7..26f396788 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,7 +11,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Upgraded `prettier` from version `3.8.3` to `3.8.4`
-## 3.15.0 - 2026-06-23
+## 3.16.0 - 2026-06-24
+
+### Added
+
+- Extended the user account settings with a copy-to-clipboard button for the user id
+- Added pagination to the platform management of the admin control panel
+- Added pagination to the tag management of the admin control panel
+- Extended the asset profile details dialog of the admin control panel with a copy-to-clipboard button for the ISIN number
+- Extended the asset profile details dialog of the admin control panel with a copy-to-clipboard button for the symbol
+
+### Changed
+
+- Improved the throughput of the market data gathering queue by applying the rate limit per data source
+- Decreased the rate limiter duration of the market data gathering queue jobs from 4 to 3 seconds
+- Removed the deprecated `SymbolProfile` field from the endpoint `GET api/v1/portfolio/holding/:dataSource/:symbol`
+- Upgraded `@simplewebauthn/browser` and `@simplewebauthn/server` from version `13.2.2` to `13.3`
+
+### Fixed
+
+- Fixed an issue with hourly market data updates not refreshing prices for asset profiles with `MANUAL` data source
+- Fixed an issue with the log context formatting in the performance logging service
+
+## 3.15.1 - 2026-06-23
### Changed
@@ -24,6 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed an issue where symbols with special characters caused API request failures by URL encoding the symbol
- Fixed the disabled state of the delete action in the asset profiles actions menu of the historical market data table in the admin control panel
- Fixed the persistence of an empty `locale` string in the scraper configuration
+- Fixed a transaction timeout that prevented gathering historical market data for symbols with a long history
+- Fixed an exception in various portfolio endpoints when historical exchange rate data is missing
## 3.14.0 - 2026-06-22
diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts
index f57507e3d..44ab67e45 100644
--- a/apps/api/src/app/activities/activities.service.ts
+++ b/apps/api/src/app/activities/activities.service.ts
@@ -746,10 +746,10 @@ export class ActivitiesService {
const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
const [
- feeInAssetProfileCurrency,
- feeInBaseCurrency,
- unitPriceInAssetProfileCurrency,
- valueInBaseCurrency
+ feeInAssetProfileCurrency = 0,
+ feeInBaseCurrency = 0,
+ unitPriceInAssetProfileCurrency = 0,
+ valueInBaseCurrency = 0
] = await Promise.all([
this.exchangeRateDataService.toCurrencyAtDate(
order.fee,
diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts
index 2ecc4d3a5..ba704d5ad 100644
--- a/apps/api/src/app/import/import.service.ts
+++ b/apps/api/src/app/import/import.service.ts
@@ -592,18 +592,19 @@ export class ImportService {
const value = new Big(quantity).mul(unitPrice).toNumber();
- const valueInBaseCurrency = this.exchangeRateDataService.toCurrencyAtDate(
- value,
- currency ?? assetProfile.currency,
- userCurrency,
- date
- );
+ const valueInBaseCurrency =
+ (await this.exchangeRateDataService.toCurrencyAtDate(
+ value,
+ currency ?? assetProfile.currency,
+ userCurrency,
+ date
+ )) ?? 0;
activities.push({
...order,
error,
value,
- valueInBaseCurrency: await valueInBaseCurrency,
+ valueInBaseCurrency,
// @ts-ignore
SymbolProfile: assetProfile
});
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index 418e60401..41697b346 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -205,21 +205,21 @@ export class PortfolioService {
switch (type) {
case ActivityType.DIVIDEND:
dividendInBaseCurrency +=
- await this.exchangeRateDataService.toCurrencyAtDate(
+ (await this.exchangeRateDataService.toCurrencyAtDate(
new Big(quantity).mul(unitPrice).toNumber(),
currency ?? SymbolProfile.currency,
userCurrency,
date
- );
+ )) ?? 0;
break;
case ActivityType.INTEREST:
interestInBaseCurrency +=
- await this.exchangeRateDataService.toCurrencyAtDate(
+ (await this.exchangeRateDataService.toCurrencyAtDate(
unitPrice,
currency ?? SymbolProfile.currency,
userCurrency,
date
- );
+ )) ?? 0;
break;
}
@@ -957,7 +957,6 @@ export class PortfolioService {
marketPrice,
marketPriceMax,
marketPriceMin,
- SymbolProfile,
tags,
assetProfile: {
assetClass: SymbolProfile.assetClass,
diff --git a/apps/api/src/interceptors/performance-logging/performance-logging.service.ts b/apps/api/src/interceptors/performance-logging/performance-logging.service.ts
index a07783cd9..b2ea03c37 100644
--- a/apps/api/src/interceptors/performance-logging/performance-logging.service.ts
+++ b/apps/api/src/interceptors/performance-logging/performance-logging.service.ts
@@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class PerformanceLoggingService {
- private readonly logger = new Logger(PerformanceLoggingService.name);
+ private readonly logger = new Logger();
public logPerformance({
className,
diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts
index 5d49848fa..10b0e6fd8 100644
--- a/apps/api/src/services/data-provider/data-provider.service.ts
+++ b/apps/api/src/services/data-provider/data-provider.service.ts
@@ -662,7 +662,11 @@ export class DataProviderService implements OnModuleInit {
);
const promise = Promise.resolve(
- dataProvider.getQuotes({ requestTimeout, symbols: symbolsChunk })
+ dataProvider.getQuotes({
+ requestTimeout,
+ useCache,
+ symbols: symbolsChunk
+ })
);
promises.push(
diff --git a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
index a55c9f328..5002fa87e 100644
--- a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
+++ b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
@@ -75,6 +75,7 @@ export interface GetHistoricalParams {
export interface GetQuotesParams {
requestTimeout?: number;
symbols: string[];
+ useCache?: boolean;
}
export interface GetSearchParams {
diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts
index 87e116dda..66571c239 100644
--- a/apps/api/src/services/data-provider/manual/manual.service.ts
+++ b/apps/api/src/services/data-provider/manual/manual.service.ts
@@ -136,7 +136,8 @@ export class ManualService implements DataProviderInterface {
}
public async getQuotes({
- symbols
+ symbols,
+ useCache = true
}: GetQuotesParams): Promise<{ [symbol: string]: DataProviderResponse }> {
const response: { [symbol: string]: DataProviderResponse } = {};
@@ -164,32 +165,32 @@ export class ManualService implements DataProviderInterface {
}
});
- const symbolProfilesWithScraperConfigurationAndInstantMode =
- symbolProfiles.filter(({ scraperConfiguration }) => {
+ const symbolProfilesToScrape = symbolProfiles.filter(
+ ({ scraperConfiguration }) => {
return (
- scraperConfiguration?.mode === 'instant' &&
+ (scraperConfiguration?.mode === 'instant' || !useCache) &&
scraperConfiguration?.selector &&
scraperConfiguration?.url
);
- });
-
- const scraperResultPromises =
- symbolProfilesWithScraperConfigurationAndInstantMode.map(
- async ({ scraperConfiguration, symbol }) => {
- try {
- const marketPrice = await this.scrape({
- scraperConfiguration,
- symbol
- });
- return { marketPrice, symbol };
- } catch (error) {
- this.logger.error(
- `Could not get quote for ${symbol} (${this.getName()}): [${error.name}] ${error.message}`
- );
- return { symbol, marketPrice: undefined };
- }
+ }
+ );
+
+ const scraperResultPromises = symbolProfilesToScrape.map(
+ async ({ scraperConfiguration, symbol }) => {
+ try {
+ const marketPrice = await this.scrape({
+ scraperConfiguration,
+ symbol
+ });
+ return { marketPrice, symbol };
+ } catch (error) {
+ this.logger.error(
+ `Could not get quote for ${symbol} (${this.getName()}): [${error.name}] ${error.message}`
+ );
+ return { symbol, marketPrice: undefined };
}
- );
+ }
+ );
// Wait for all scraping requests to complete concurrently
const scraperResults = await Promise.all(scraperResultPromises);
diff --git a/apps/api/src/services/market-data/market-data.service.ts b/apps/api/src/services/market-data/market-data.service.ts
index 27c741055..086434724 100644
--- a/apps/api/src/services/market-data/market-data.service.ts
+++ b/apps/api/src/services/market-data/market-data.service.ts
@@ -1,6 +1,7 @@
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
+import { DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_TIMEOUT } from '@ghostfolio/common/config';
import { UpdateMarketDataDto } from '@ghostfolio/common/dtos';
import { resetHours } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
@@ -155,49 +156,52 @@ export class MarketDataService {
dataSource,
symbol
}: AssetProfileIdentifier & { data: Prisma.MarketDataUpdateInput[] }) {
- await this.prismaService.$transaction(async (prisma) => {
- if (data.length > 0) {
- let minTime = Infinity;
- let maxTime = -Infinity;
+ await this.prismaService.$transaction(
+ async (prisma) => {
+ if (data.length > 0) {
+ let minTime = Infinity;
+ let maxTime = -Infinity;
- for (const { date } of data) {
- const time = (date as Date).getTime();
+ for (const { date } of data) {
+ const time = (date as Date).getTime();
- if (time < minTime) {
- minTime = time;
- }
+ if (time < minTime) {
+ minTime = time;
+ }
- if (time > maxTime) {
- maxTime = time;
+ if (time > maxTime) {
+ maxTime = time;
+ }
}
- }
- const minDate = new Date(minTime);
- const maxDate = new Date(maxTime);
+ const minDate = new Date(minTime);
+ const maxDate = new Date(maxTime);
- await prisma.marketData.deleteMany({
- where: {
- dataSource,
- symbol,
- date: {
- gte: minDate,
- lte: maxDate
+ await prisma.marketData.deleteMany({
+ where: {
+ dataSource,
+ symbol,
+ date: {
+ gte: minDate,
+ lte: maxDate
+ }
}
- }
- });
+ });
- await prisma.marketData.createMany({
- data: data.map(({ date, marketPrice, state }) => ({
- dataSource,
- symbol,
- date: date as Date,
- marketPrice: marketPrice as number,
- state: state as MarketDataState
- })),
- skipDuplicates: true
- });
- }
- });
+ await prisma.marketData.createMany({
+ data: data.map(({ date, marketPrice, state }) => ({
+ dataSource,
+ symbol,
+ date: date as Date,
+ marketPrice: marketPrice as number,
+ state: state as MarketDataState
+ })),
+ skipDuplicates: true
+ });
+ }
+ },
+ { timeout: DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_TIMEOUT }
+ );
}
public async updateAssetProfileIdentifier(
diff --git a/apps/api/src/services/queues/data-gathering/data-gathering.module.ts b/apps/api/src/services/queues/data-gathering/data-gathering.module.ts
index 5ac6c40c0..d66411797 100644
--- a/apps/api/src/services/queues/data-gathering/data-gathering.module.ts
+++ b/apps/api/src/services/queues/data-gathering/data-gathering.module.ts
@@ -28,7 +28,8 @@ import { DataGatheringProcessor } from './data-gathering.processor';
}),
BullModule.registerQueue({
limiter: {
- duration: ms('4 seconds'),
+ duration: ms('3 seconds'),
+ groupKey: 'dataSource',
max: 1
},
name: DATA_GATHERING_QUEUE
diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
index f00c279a0..9b69ef6fc 100644
--- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
+++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
@@ -165,7 +165,11 @@
} @else {
- Symbol
@@ -202,6 +206,7 @@
ISIN
|
+
+
diff --git a/apps/client/src/app/components/admin-platform/admin-platform.component.ts b/apps/client/src/app/components/admin-platform/admin-platform.component.ts
index 26e6c2b1e..727585478 100644
--- a/apps/client/src/app/components/admin-platform/admin-platform.component.ts
+++ b/apps/client/src/app/components/admin-platform/admin-platform.component.ts
@@ -1,4 +1,5 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
+import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { CreatePlatformDto, UpdatePlatformDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getLocale, getLowercase } from '@ghostfolio/common/helper';
@@ -22,6 +23,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
+import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
@@ -46,6 +48,7 @@ import { CreateOrUpdatePlatformDialogParams } from './create-or-update-platform-
IonIcon,
MatButtonModule,
MatMenuModule,
+ MatPaginatorModule,
MatSortModule,
MatTableModule,
RouterModule
@@ -59,11 +62,13 @@ export class GfAdminPlatformComponent implements OnInit {
protected dataSource = new MatTableDataSource
();
protected readonly displayedColumns = ['name', 'url', 'accounts', 'actions'];
+ protected readonly pageSize = DEFAULT_PAGE_SIZE;
protected platforms: Platform[];
private readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
);
+ private readonly paginator = viewChild.required(MatPaginator);
private readonly sort = viewChild.required(MatSort);
private readonly adminService = inject(AdminService);
@@ -145,6 +150,7 @@ export class GfAdminPlatformComponent implements OnInit {
this.platforms = platforms;
this.dataSource = new MatTableDataSource(platforms);
+ this.dataSource.paginator = this.paginator();
this.dataSource.sort = this.sort();
this.dataSource.sortingDataAccessor = getLowercase;
diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.scss b/apps/client/src/app/components/admin-settings/admin-settings.component.scss
index 1c0a17624..29340ea0d 100644
--- a/apps/client/src/app/components/admin-settings/admin-settings.component.scss
+++ b/apps/client/src/app/components/admin-settings/admin-settings.component.scss
@@ -10,7 +10,33 @@
}
.mat-mdc-card {
- --mat-card-outlined-container-color: whitesmoke;
+ --gradient-opacity: 50%;
+ --mat-card-outlined-outline-color: rgb(193, 219, 254);
+
+ background-color: white;
+ background-image: linear-gradient(
+ 109.6deg,
+ color-mix(in srgb, rgba(255, 255, 255, 1) 100%, transparent) 11.2%,
+ color-mix(
+ in srgb,
+ rgba(221, 108, 241, 0.26) var(--gradient-opacity),
+ transparent
+ )
+ 42%,
+ color-mix(
+ in srgb,
+ rgba(229, 106, 253, 0.71) var(--gradient-opacity),
+ transparent
+ )
+ 71.5%,
+ color-mix(
+ in srgb,
+ rgba(123, 183, 253, 1) var(--gradient-opacity),
+ transparent
+ )
+ 100%
+ );
+ color: rgb(var(--dark-primary-text));
.mat-mdc-card-actions {
min-height: 0;
@@ -32,6 +58,6 @@
:host-context(.theme-dark) {
.mat-mdc-card {
- --mat-card-outlined-container-color: #222222;
+ --mat-card-outlined-outline-color: white;
}
}
diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.html b/apps/client/src/app/components/admin-tag/admin-tag.component.html
index 3c125d5c0..a84cbb283 100644
--- a/apps/client/src/app/components/admin-tag/admin-tag.component.html
+++ b/apps/client/src/app/components/admin-tag/admin-tag.component.html
@@ -89,3 +89,9 @@
+
+
diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.ts b/apps/client/src/app/components/admin-tag/admin-tag.component.ts
index fcb901707..7bd69c964 100644
--- a/apps/client/src/app/components/admin-tag/admin-tag.component.ts
+++ b/apps/client/src/app/components/admin-tag/admin-tag.component.ts
@@ -1,4 +1,5 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
+import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { CreateTagDto, UpdateTagDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getLocale, getLowercase } from '@ghostfolio/common/helper';
@@ -21,6 +22,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
+import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
@@ -44,6 +46,7 @@ import { CreateOrUpdateTagDialogParams } from './create-or-update-tag-dialog/int
IonIcon,
MatButtonModule,
MatMenuModule,
+ MatPaginatorModule,
MatSortModule,
MatTableModule,
RouterModule
@@ -62,11 +65,13 @@ export class GfAdminTagComponent implements OnInit {
'activities',
'actions'
];
+ protected readonly pageSize = DEFAULT_PAGE_SIZE;
protected tags: Tag[];
private readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
);
+ private readonly paginator = viewChild.required(MatPaginator);
private readonly sort = viewChild.required(MatSort);
private readonly changeDetectorRef = inject(ChangeDetectorRef);
@@ -147,6 +152,7 @@ export class GfAdminTagComponent implements OnInit {
this.tags = tags;
this.dataSource = new MatTableDataSource(this.tags);
+ this.dataSource.paginator = this.paginator();
this.dataSource.sort = this.sort();
this.dataSource.sortingDataAccessor = getLowercase;
diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
index e14c638be..6fa3a4853 100644
--- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
+++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
@@ -116,6 +116,19 @@ export class GfHoldingDetailDialogComponent implements OnInit {
protected accounts: Account[];
protected activitiesCount: number;
protected assetClass: string;
+ protected assetProfile: Pick<
+ EnhancedSymbolProfile,
+ | 'assetClass'
+ | 'assetSubClass'
+ | 'countries'
+ | 'currency'
+ | 'dataSource'
+ | 'isin'
+ | 'name'
+ | 'sectors'
+ | 'symbol'
+ | 'userId'
+ >;
protected assetSubClass: string;
protected averagePrice: number;
protected averagePricePrecision = 2;
@@ -164,7 +177,6 @@ export class GfHoldingDetailDialogComponent implements OnInit {
};
protected sortColumn = 'date';
protected sortDirection: SortDirection = 'desc';
- protected SymbolProfile: EnhancedSymbolProfile;
protected tagsAvailable: Tag[];
protected readonly translate = translate;
protected user: User;
@@ -266,6 +278,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
.subscribe(
({
activitiesCount,
+ assetProfile,
averagePrice,
dataProviderInfo,
dateOfFirstActivity,
@@ -280,11 +293,11 @@ export class GfHoldingDetailDialogComponent implements OnInit {
netPerformancePercentWithCurrencyEffect,
netPerformanceWithCurrencyEffect,
quantity,
- SymbolProfile,
tags,
value
}) => {
this.activitiesCount = activitiesCount;
+ this.assetProfile = assetProfile;
this.averagePrice = averagePrice;
if (
@@ -322,8 +335,8 @@ export class GfHoldingDetailDialogComponent implements OnInit {
this.user?.permissions,
permissions.readMarketDataOfOwnAssetProfile
) &&
- SymbolProfile?.dataSource === 'MANUAL' &&
- SymbolProfile?.userId === this.user?.id;
+ assetProfile?.dataSource === 'MANUAL' &&
+ assetProfile?.userId === this.user?.id;
this.historicalDataItems = historicalData.map(
({ averagePrice, date, marketPrice }) => {
@@ -402,7 +415,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
if (Number.isInteger(this.quantity)) {
this.quantityPrecision = 0;
- } else if (SymbolProfile?.assetSubClass === 'CRYPTOCURRENCY') {
+ } else if (assetProfile?.assetSubClass === 'CRYPTOCURRENCY') {
if (this.quantity < 10) {
this.quantityPrecision = 8;
} else if (this.quantity < 1000) {
@@ -413,7 +426,6 @@ export class GfHoldingDetailDialogComponent implements OnInit {
}
this.sectors = {};
- this.SymbolProfile = SymbolProfile;
this.tags = tags.map((tag) => {
return {
@@ -427,21 +439,21 @@ export class GfHoldingDetailDialogComponent implements OnInit {
this.value = value;
const reportDataGlitchSubject = `Ghostfolio Data Glitch Report${
- this.SymbolProfile?.symbol ? ` (${this.SymbolProfile.symbol})` : ''
+ this.assetProfile?.symbol ? ` (${this.assetProfile.symbol})` : ''
}`;
- this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=${reportDataGlitchSubject}&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${this.SymbolProfile?.symbol}%0DData Source: ${this.SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`;
+ this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=${reportDataGlitchSubject}&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${this.assetProfile?.symbol}%0DData Source: ${this.assetProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`;
- if (this.SymbolProfile?.assetClass) {
- this.assetClass = translate(this.SymbolProfile?.assetClass);
+ if (this.assetProfile?.assetClass) {
+ this.assetClass = translate(this.assetProfile?.assetClass);
}
- if (this.SymbolProfile?.assetSubClass) {
- this.assetSubClass = translate(this.SymbolProfile?.assetSubClass);
+ if (this.assetProfile?.assetSubClass) {
+ this.assetSubClass = translate(this.assetProfile?.assetSubClass);
}
- if (this.SymbolProfile?.countries?.length > 0) {
- for (const country of this.SymbolProfile.countries) {
+ if (this.assetProfile?.countries?.length > 0) {
+ for (const country of this.assetProfile.countries) {
this.countries[country.code] = {
name: getCountryName({ code: country.code }),
value: country.weight
@@ -449,8 +461,8 @@ export class GfHoldingDetailDialogComponent implements OnInit {
}
}
- if (this.SymbolProfile?.sectors?.length > 0) {
- for (const sector of this.SymbolProfile.sectors) {
+ if (this.assetProfile?.sectors?.length > 0) {
+ for (const sector of this.assetProfile.sectors) {
this.sectors[sector.name] = {
name: translate(sector.name),
value: sector.weight
@@ -570,12 +582,12 @@ export class GfHoldingDetailDialogComponent implements OnInit {
const activity: CreateOrderDto = {
accountId: this.accounts.length === 1 ? this.accounts[0].id : undefined,
comment: undefined,
- currency: this.SymbolProfile?.currency ?? '',
- dataSource: this.SymbolProfile?.dataSource,
+ currency: this.assetProfile?.currency ?? '',
+ dataSource: this.assetProfile?.dataSource,
date: today.toISOString(),
fee: 0,
quantity: this.quantity,
- symbol: this.SymbolProfile?.symbol ?? '',
+ symbol: this.assetProfile?.symbol ?? '',
tags: this.tags.map(({ id }) => {
return id;
}),
@@ -606,7 +618,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
.subscribe((data) => {
downloadAsFile({
content: data,
- fileName: `ghostfolio-export-${this.SymbolProfile?.symbol}-${format(
+ fileName: `ghostfolio-export-${this.assetProfile?.symbol}-${format(
parseISO(data.meta.date),
'yyyyMMddHHmm'
)}.json`,
diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
index 0d5691874..ff1d19cc3 100644
--- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
+++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
@@ -1,7 +1,7 @@
@@ -24,11 +24,11 @@
[benchmarkDataItems]="benchmarkDataItems"
[benchmarkLabel]="benchmarkLabel"
[colorScheme]="data.colorScheme"
- [currency]="SymbolProfile?.currency"
+ [currency]="assetProfile?.currency"
[historicalDataItems]="historicalDataItems"
[isAnimated]="true"
[label]="
- isUUID(data.symbol) ? (SymbolProfile?.name ?? data.symbol) : data.symbol
+ isUUID(data.symbol) ? (assetProfile?.name ?? data.symbol) : data.symbol
"
[locale]="data.locale"
[showGradient]="true"
@@ -60,8 +60,8 @@
[value]="netPerformanceWithCurrencyEffect"
>
@if (
- SymbolProfile?.currency &&
- data.baseCurrency !== SymbolProfile?.currency
+ assetProfile?.currency &&
+ data.baseCurrency !== assetProfile?.currency
) {
Change with currency effect
} @else {
@@ -80,8 +80,8 @@
[value]="netPerformancePercentWithCurrencyEffect"
>
@if (
- SymbolProfile?.currency &&
- data.baseCurrency !== SymbolProfile?.currency
+ assetProfile?.currency &&
+ data.baseCurrency !== assetProfile?.currency
) {
Performance with currency effect
} @else {
@@ -96,7 +96,7 @@
[isCurrency]="true"
[locale]="data.locale"
[precision]="averagePricePrecision"
- [unit]="SymbolProfile?.currency"
+ [unit]="assetProfile?.currency"
[value]="averagePrice"
>Average Unit Price
@@ -108,7 +108,7 @@
[isCurrency]="true"
[locale]="data.locale"
[precision]="marketPricePrecision"
- [unit]="SymbolProfile?.currency"
+ [unit]="assetProfile?.currency"
[value]="marketPrice"
>Market Price
@@ -124,7 +124,7 @@
[isCurrency]="true"
[locale]="data.locale"
[precision]="marketPriceMinPrecision"
- [unit]="SymbolProfile?.currency"
+ [unit]="assetProfile?.currency"
[value]="marketPriceMin"
>Minimum Price
@@ -140,7 +140,7 @@
[isCurrency]="true"
[locale]="data.locale"
[precision]="marketPriceMaxPrecision"
- [unit]="SymbolProfile?.currency"
+ [unit]="assetProfile?.currency"
[value]="marketPriceMax"
>Maximum Price
@@ -250,23 +250,23 @@
>
@if (
- SymbolProfile?.countries?.length > 0 ||
- SymbolProfile?.sectors?.length > 0
+ assetProfile?.countries?.length > 0 ||
+ assetProfile?.sectors?.length > 0
) {
@if (
- SymbolProfile?.countries?.length === 1 &&
- SymbolProfile?.sectors?.length === 1
+ assetProfile?.countries?.length === 1 &&
+ assetProfile?.sectors?.length === 1
) {
Sector
- @if (SymbolProfile?.countries?.length === 1) {
+ @if (assetProfile?.countries?.length === 1) {
CountrySymbol
@@ -322,8 +322,8 @@
ISIN
@@ -404,12 +404,12 @@
Market Data
@@ -462,8 +462,8 @@
mat-stroked-button
[queryParams]="{
assetProfileDialog: true,
- dataSource: SymbolProfile?.dataSource,
- symbol: SymbolProfile?.symbol
+ dataSource: assetProfile?.dataSource,
+ symbol: assetProfile?.symbol
}"
[routerLink]="routerLinkAdminControlMarketData"
(click)="onClose()"
diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
index 72bbfc2c6..0101bb90a 100644
--- a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
+++ b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
@@ -12,6 +12,7 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services';
+import { GfValueComponent } from '@ghostfolio/ui/value';
import {
ChangeDetectionStrategy,
@@ -51,6 +52,7 @@ import { catchError } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
FormsModule,
+ GfValueComponent,
IonIcon,
MatButtonModule,
MatCardModule,
diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.html b/apps/client/src/app/components/user-account-settings/user-account-settings.html
index f646ef0fd..e4e9777de 100644
--- a/apps/client/src/app/components/user-account-settings/user-account-settings.html
+++ b/apps/client/src/app/components/user-account-settings/user-account-settings.html
@@ -260,7 +260,13 @@
Ghostfolio User ID
- {{ user?.id }}
+
+
+
diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts
index e6d717c7b..03600088f 100644
--- a/libs/common/src/lib/config.ts
+++ b/libs/common/src/lib/config.ts
@@ -90,9 +90,12 @@ export const DEFAULT_PAGE_SIZE = 50;
export const DEFAULT_PORT = 3333;
export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1;
export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY = 1;
+export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_TIMEOUT =
+ ms('1 minute');
export const DEFAULT_PROCESSOR_GATHER_STATISTICS_CONCURRENCY = 1;
export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1;
-export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000;
+export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT =
+ ms('30 seconds');
export const DEFAULT_REDACTED_PATHS = [
'accounts[*].balance',
diff --git a/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
index 3b07666c9..075234e6a 100644
--- a/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
+++ b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
@@ -44,10 +44,6 @@ export interface PortfolioHoldingResponse {
netPerformanceWithCurrencyEffect: number;
performances: Benchmark['performances'];
quantity: number;
-
- /* @deprecated */
- SymbolProfile: EnhancedSymbolProfile;
-
tags: Tag[];
value: number;
}
diff --git a/package-lock.json b/package-lock.json
index c1f3310dc..c55ff5ef8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ghostfolio",
- "version": "3.15.0",
+ "version": "3.16.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ghostfolio",
- "version": "3.15.0",
+ "version": "3.16.0",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
@@ -43,8 +43,8 @@
"@openrouter/ai-sdk-provider": "2.9.1",
"@prisma/adapter-pg": "7.8.0",
"@prisma/client": "7.8.0",
- "@simplewebauthn/browser": "13.2.2",
- "@simplewebauthn/server": "13.2.2",
+ "@simplewebauthn/browser": "13.3.0",
+ "@simplewebauthn/server": "13.3.1",
"ai": "6.0.174",
"alphavantage": "2.2.0",
"big.js": "7.0.1",
@@ -129,7 +129,7 @@
"@storybook/addon-themes": "10.1.10",
"@storybook/angular": "10.1.10",
"@trivago/prettier-plugin-sort-imports": "6.0.2",
- "@types/big.js": "6.2.2",
+ "@types/big.js": "7.0.0",
"@types/cookie-parser": "1.4.10",
"@types/fast-redact": "3.0.4",
"@types/google-spreadsheet": "3.1.5",
@@ -13296,25 +13296,25 @@
}
},
"node_modules/@simplewebauthn/browser": {
- "version": "13.2.2",
- "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.2.tgz",
- "integrity": "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==",
+ "version": "13.3.0",
+ "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz",
+ "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==",
"license": "MIT"
},
"node_modules/@simplewebauthn/server": {
- "version": "13.2.2",
- "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.2.2.tgz",
- "integrity": "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==",
+ "version": "13.3.1",
+ "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.1.tgz",
+ "integrity": "sha512-GV/oM/qeycWn8p42JZIMJBsXWQcNFg+nJFzeQTnMA4gN8mXg0+HZFWJerHg8ZN/zlveMS3iV1wzuFpOVWS/46w==",
"license": "MIT",
"dependencies": {
"@hexagon/base64": "^1.1.27",
"@levischuck/tiny-cbor": "^0.2.2",
- "@peculiar/asn1-android": "^2.3.10",
- "@peculiar/asn1-ecc": "^2.3.8",
- "@peculiar/asn1-rsa": "^2.3.8",
- "@peculiar/asn1-schema": "^2.3.8",
- "@peculiar/asn1-x509": "^2.3.8",
- "@peculiar/x509": "^1.13.0"
+ "@peculiar/asn1-android": "^2.6.0",
+ "@peculiar/asn1-ecc": "^2.6.1",
+ "@peculiar/asn1-rsa": "^2.6.1",
+ "@peculiar/asn1-schema": "^2.6.0",
+ "@peculiar/asn1-x509": "^2.6.1",
+ "@peculiar/x509": "^1.14.3"
},
"engines": {
"node": ">=20.0.0"
@@ -14020,9 +14020,9 @@
}
},
"node_modules/@types/big.js": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.2.2.tgz",
- "integrity": "sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA==",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-7.0.0.tgz",
+ "integrity": "sha512-WfAGp7IbJvyB8EmWK4tJD24rJRAL6uVbw3LV/hJntFNam+os9KWKj0PzXo8rRRpjupYK8U0M8FoBB8dBhWF2dg==",
"dev": true,
"license": "MIT"
},
diff --git a/package.json b/package.json
index 6fe206f01..dee237dd4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "3.15.0",
+ "version": "3.16.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@@ -87,8 +87,8 @@
"@openrouter/ai-sdk-provider": "2.9.1",
"@prisma/adapter-pg": "7.8.0",
"@prisma/client": "7.8.0",
- "@simplewebauthn/browser": "13.2.2",
- "@simplewebauthn/server": "13.2.2",
+ "@simplewebauthn/browser": "13.3.0",
+ "@simplewebauthn/server": "13.3.1",
"ai": "6.0.174",
"alphavantage": "2.2.0",
"big.js": "7.0.1",
@@ -173,7 +173,7 @@
"@storybook/addon-themes": "10.1.10",
"@storybook/angular": "10.1.10",
"@trivago/prettier-plugin-sort-imports": "6.0.2",
- "@types/big.js": "6.2.2",
+ "@types/big.js": "7.0.0",
"@types/cookie-parser": "1.4.10",
"@types/fast-redact": "3.0.4",
"@types/google-spreadsheet": "3.1.5",