Browse Source

Merge branch 'main' into task/upgrade-prettier-to-version-3.8.4

pull/7051/head
Thomas Kaul 3 weeks ago
committed by GitHub
parent
commit
20b0705a3c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 26
      CHANGELOG.md
  2. 8
      apps/api/src/app/activities/activities.service.ts
  3. 15
      apps/api/src/app/import/import.service.ts
  4. 9
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 2
      apps/api/src/interceptors/performance-logging/performance-logging.service.ts
  6. 6
      apps/api/src/services/data-provider/data-provider.service.ts
  7. 1
      apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
  8. 45
      apps/api/src/services/data-provider/manual/manual.service.ts
  9. 74
      apps/api/src/services/market-data/market-data.service.ts
  10. 3
      apps/api/src/services/queues/data-gathering/data-gathering.module.ts
  11. 7
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  12. 6
      apps/client/src/app/components/admin-platform/admin-platform.component.html
  13. 6
      apps/client/src/app/components/admin-platform/admin-platform.component.ts
  14. 30
      apps/client/src/app/components/admin-settings/admin-settings.component.scss
  15. 6
      apps/client/src/app/components/admin-tag/admin-tag.component.html
  16. 6
      apps/client/src/app/components/admin-tag/admin-tag.component.ts
  17. 52
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  18. 54
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  19. 2
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  20. 8
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  21. 5
      libs/common/src/lib/config.ts
  22. 4
      libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts
  23. 40
      package-lock.json
  24. 8
      package.json

26
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

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

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

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

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

6
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(

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

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

74
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(

3
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

7
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -165,7 +165,11 @@
</div>
} @else {
<div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="assetProfile?.symbol"
<gf-value
i18n
size="medium"
[enableCopyToClipboardButton]="true"
[value]="assetProfile?.symbol"
>Symbol</gf-value
>
</div>
@ -202,6 +206,7 @@
<div class="col-6 mb-3">
<gf-value
size="medium"
[enableCopyToClipboardButton]="true"
[hidden]="!assetProfile?.isin"
[value]="assetProfile?.isin"
>ISIN</gf-value

6
apps/client/src/app/components/admin-platform/admin-platform.component.html

@ -96,3 +96,9 @@
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>
<mat-paginator
[class.d-none]="dataSource.data.length <= pageSize"
[hidePageSize]="true"
[pageSize]="pageSize"
/>

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

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

6
apps/client/src/app/components/admin-tag/admin-tag.component.html

@ -89,3 +89,9 @@
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>
<mat-paginator
[class.d-none]="dataSource.data.length <= pageSize"
[hidePageSize]="true"
[pageSize]="pageSize"
/>

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

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

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

@ -1,7 +1,7 @@
<gf-dialog-header
position="center"
[deviceType]="data.deviceType"
[title]="SymbolProfile?.name ?? SymbolProfile?.symbol"
[title]="assetProfile?.name ?? assetProfile?.symbol"
(closeButtonClicked)="onClose()"
/>
@ -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</gf-value
>
@ -108,7 +108,7 @@
[isCurrency]="true"
[locale]="data.locale"
[precision]="marketPricePrecision"
[unit]="SymbolProfile?.currency"
[unit]="assetProfile?.currency"
[value]="marketPrice"
>Market Price</gf-value
>
@ -124,7 +124,7 @@
[isCurrency]="true"
[locale]="data.locale"
[precision]="marketPriceMinPrecision"
[unit]="SymbolProfile?.currency"
[unit]="assetProfile?.currency"
[value]="marketPriceMin"
>Minimum Price</gf-value
>
@ -140,7 +140,7 @@
[isCurrency]="true"
[locale]="data.locale"
[precision]="marketPriceMaxPrecision"
[unit]="SymbolProfile?.currency"
[unit]="assetProfile?.currency"
[value]="marketPriceMax"
>Maximum Price</gf-value
>
@ -250,23 +250,23 @@
>
</div>
@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
) {
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="translate(SymbolProfile.sectors[0].name)"
[value]="translate(assetProfile.sectors[0].name)"
>Sector</gf-value
>
</div>
@if (SymbolProfile?.countries?.length === 1) {
@if (assetProfile?.countries?.length === 1) {
<div class="col-6 mb-3">
<gf-value
i18n
@ -274,7 +274,7 @@
[locale]="data.locale"
[value]="
getCountryName({
code: SymbolProfile.countries[0].code
code: assetProfile.countries[0].code
})
"
>Country</gf-value
@ -313,8 +313,8 @@
i18n
size="medium"
[enableCopyToClipboardButton]="true"
[hidden]="!SymbolProfile?.symbol"
[value]="SymbolProfile?.symbol"
[hidden]="!assetProfile?.symbol"
[value]="assetProfile?.symbol"
>Symbol</gf-value
>
</div>
@ -322,8 +322,8 @@
<gf-value
size="medium"
[enableCopyToClipboardButton]="true"
[hidden]="!SymbolProfile?.isin"
[value]="SymbolProfile?.isin"
[hidden]="!assetProfile?.isin"
[value]="assetProfile?.isin"
>ISIN</gf-value
>
</div>
@ -404,12 +404,12 @@
<div class="d-none d-sm-block ml-2" i18n>Market Data</div>
</ng-template>
<gf-historical-market-data-editor
[currency]="SymbolProfile?.currency"
[dataSource]="SymbolProfile?.dataSource"
[currency]="assetProfile?.currency"
[dataSource]="assetProfile?.dataSource"
[dateOfFirstActivity]="dateOfFirstActivity"
[locale]="data.locale"
[marketData]="marketDataItems"
[symbol]="SymbolProfile?.symbol"
[symbol]="assetProfile?.symbol"
[user]="user"
(marketDataChanged)="onMarketDataChanged($event)"
/>
@ -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()"

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

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

@ -260,7 +260,13 @@
<div class="pr-1 w-50">
Ghostfolio <ng-container i18n>User ID</ng-container>
</div>
<div class="pl-1 text-monospace w-50">{{ user?.id }}</div>
<div class="pl-1 w-50">
<gf-value
class="text-monospace"
[enableCopyToClipboardButton]="true"
[value]="user?.id"
/>
</div>
</div>
<div class="align-items-center d-flex py-1">
<div class="pr-1 w-50"></div>

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

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

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

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

Loading…
Cancel
Save