From 7b262aeeaacea6217dce2f90b19a60269f7ba107 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:34:03 +0200 Subject: [PATCH] Fix creation of asset profiles with symbol in wrong letter case by using original symbol --- .../app/activities/activities.controller.ts | 23 +++++++++++----- .../symbol-profile/symbol-profile.service.ts | 27 +++++++++++++++++++ .../admin-market-data.component.ts | 19 ++++++++----- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/apps/api/src/app/activities/activities.controller.ts b/apps/api/src/app/activities/activities.controller.ts index 5d0d932ee..7b647ba73 100644 --- a/apps/api/src/app/activities/activities.controller.ts +++ b/apps/api/src/app/activities/activities.controller.ts @@ -8,6 +8,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; +import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config'; import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos'; @@ -47,7 +48,8 @@ export class ActivitiesController { private readonly activitiesService: ActivitiesService, private readonly apiService: ApiService, private readonly dataProviderService: DataProviderService, - private readonly dataGatheringService: DataGatheringService + private readonly dataGatheringService: DataGatheringService, + private readonly symbolProfileService: SymbolProfileService ) {} @Delete() @@ -256,10 +258,14 @@ export class ActivitiesController { const customCurrency = data.customCurrency; const dataSource = data.dataSource; - const symbol = - assetProfiles[ - getAssetProfileIdentifier({ dataSource, symbol: data.symbol }) - ]?.symbol ?? data.symbol; + const symbol = await this.symbolProfileService.getSymbolOfAssetProfile({ + dataSource, + symbol: data.symbol, + symbolOfDataProvider: + assetProfiles[ + getAssetProfileIdentifier({ dataSource, symbol: data.symbol }) + ]?.symbol + }); if (customCurrency) { data.currency = customCurrency; @@ -340,6 +346,11 @@ export class ActivitiesController { const customCurrency = data.customCurrency; const dataSource = data.dataSource; + const symbol = await this.symbolProfileService.getSymbolOfAssetProfile({ + dataSource, + symbol: data.symbol + }); + delete data.accountId; if (customCurrency) { @@ -366,7 +377,7 @@ export class ActivitiesController { connect: { dataSource_symbol: { dataSource, - symbol: data.symbol + symbol } }, update: { diff --git a/apps/api/src/services/symbol-profile/symbol-profile.service.ts b/apps/api/src/services/symbol-profile/symbol-profile.service.ts index ebc8a94c7..970802b5a 100644 --- a/apps/api/src/services/symbol-profile/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile/symbol-profile.service.ts @@ -126,6 +126,33 @@ export class SymbolProfileService { }); } + /** + * Gets the symbol to use for an asset profile. An asset profile which is + * already in the database wins, also if its symbol has a different letter + * case. This prevents a second asset profile for the same instrument. + * Otherwise the symbol of the data provider is used, because it has the + * correct letter case. A custom asset profile (MANUAL) belongs to a user, + * thus its symbol stays unchanged. + */ + public async getSymbolOfAssetProfile({ + dataSource, + symbol, + symbolOfDataProvider + }: { symbolOfDataProvider?: string } & AssetProfileIdentifier) { + if (dataSource === DataSource.MANUAL) { + return symbol; + } + + const symbolProfile = await this.prismaService.symbolProfile.findFirst({ + where: { + dataSource, + symbol: { equals: symbol, mode: 'insensitive' } + } + }); + + return symbolProfile?.symbol ?? symbolOfDataProvider ?? symbol; + } + public async getSymbolProfiles( aAssetProfileIdentifiers: AssetProfileIdentifier[] ): Promise { diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index 9c3c0c791..ef70590ba 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -496,13 +496,18 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit { this.adminService .addAssetProfile({ dataSource, symbol }) .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((assetProfile) => { - this.loadData(); - - this.onOpenAssetProfileDialog({ - dataSource, - symbol: assetProfile?.symbol ?? symbol - }); + .subscribe({ + error: () => { + this.router.navigate(['.'], { relativeTo: this.route }); + }, + next: (assetProfile) => { + this.loadData(); + + this.onOpenAssetProfileDialog({ + dataSource, + symbol: assetProfile?.symbol ?? symbol + }); + } }); } else { this.loadData();