From 3a4b0ce304793ba0e1167d2dd11517013409c353 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 6 Jun 2026 17:41:13 +0200 Subject: [PATCH] Task/harmonize sector names accross data providers (#6994) * Harmonize sector names * Update changelog --- CHANGELOG.md | 2 + apps/api/src/helper/sector.helper.ts | 28 ++++++++ .../trackinsight/trackinsight.service.ts | 15 +++-- .../yahoo-finance/yahoo-finance.service.ts | 66 ++++++------------- .../asset-profile-dialog.component.ts | 4 +- .../asset-profile-dialog.html | 2 +- .../holding-detail-dialog.component.ts | 3 +- .../holding-detail-dialog.html | 2 +- .../allocations/allocations-page.component.ts | 2 +- .../app/pages/public/public-page.component.ts | 3 +- libs/common/src/lib/config.ts | 15 +++++ libs/common/src/lib/types/index.ts | 2 + libs/common/src/lib/types/sector-name.type.ts | 3 + libs/ui/src/lib/i18n.ts | 20 +++++- 14 files changed, 110 insertions(+), 57 deletions(-) create mode 100644 apps/api/src/helper/sector.helper.ts create mode 100644 libs/common/src/lib/types/sector-name.type.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 897d56d29..8e13d948b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Harmonized the sector names across the data providers +- Localized the sector names - Centralized the asset profile override logic for manual adjustments - Improved the styling in the user detail dialog of the admin control panel’s users section - Prevented the deletion of asset profiles that are currently in use diff --git a/apps/api/src/helper/sector.helper.ts b/apps/api/src/helper/sector.helper.ts new file mode 100644 index 000000000..e0face386 --- /dev/null +++ b/apps/api/src/helper/sector.helper.ts @@ -0,0 +1,28 @@ +import { SECTORS } from '@ghostfolio/common/config'; +import { SectorName } from '@ghostfolio/common/types'; + +import { Logger } from '@nestjs/common'; + +export function getSectorName({ + aliases = {}, + name +}: { + aliases?: Record; + name: string; +}): SectorName { + if (aliases[name]) { + return aliases[name]; + } + + if ((SECTORS as readonly string[]).includes(name)) { + return name as SectorName; + } + + if (name) { + const logger = new Logger('getSectorName'); + + logger.warn(`Could not map the sector "${name}" to the ontology`); + } + + return 'Other'; +} diff --git a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts index 3d42de443..c8291a901 100644 --- a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts @@ -1,10 +1,12 @@ import { getCountryCodeByName } from '@ghostfolio/api/helper/country.helper'; +import { getSectorName } from '@ghostfolio/api/helper/sector.helper'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service'; import { Holding } from '@ghostfolio/common/interfaces'; import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; +import { SectorName } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; import { SymbolProfile } from '@prisma/client'; @@ -17,11 +19,13 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { USA: 'United States' }; private static holdingsWeightTreshold = 0.85; - private static sectorsMapping = { + private static sectorsMapping: Record = { 'Consumer Discretionary': 'Consumer Cyclical', - 'Consumer Defensive': 'Consumer Staples', + 'Consumer Staples': 'Consumer Defensive', + Financials: 'Financial Services', 'Health Care': 'Healthcare', - 'Information Technology': 'Technology' + 'Information Technology': 'Technology', + Materials: 'Basic Materials' }; private readonly logger = new Logger(TrackinsightDataEnhancerService.name); @@ -155,7 +159,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { holdings?.sectors ?? {} )) { response.sectors.push({ - name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name, + name: getSectorName({ + name, + aliases: TrackinsightDataEnhancerService.sectorsMapping + }), weight: value.weight }); } diff --git a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts index 034916a5f..4fb0e96ed 100644 --- a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts @@ -1,12 +1,13 @@ +import { getSectorName } from '@ghostfolio/api/helper/sector.helper'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { DEFAULT_CURRENCY, - REPLACE_NAME_PARTS, - UNKNOWN_KEY + REPLACE_NAME_PARTS } from '@ghostfolio/common/config'; import { isCurrency } from '@ghostfolio/common/helper'; +import { SectorName } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; import { @@ -23,6 +24,20 @@ import type { Price } from 'yahoo-finance2/esm/src/modules/quoteSummary-iface'; @Injectable() export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { + private static sectorsMapping: Record = { + basic_materials: 'Basic Materials', + communication_services: 'Communication Services', + consumer_cyclical: 'Consumer Cyclical', + consumer_defensive: 'Consumer Defensive', + energy: 'Energy', + financial_services: 'Financial Services', + healthcare: 'Healthcare', + industrials: 'Industrials', + realestate: 'Real Estate', + technology: 'Technology', + utilities: 'Utilities' + }; + private readonly logger = new Logger(YahooFinanceDataEnhancerService.name); private readonly yahooFinance = new YahooFinance({ @@ -224,7 +239,10 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { .flatMap((sectorWeighting) => { return Object.entries(sectorWeighting).map(([sector, weight]) => { return { - name: this.parseSector(sector), + name: getSectorName({ + aliases: YahooFinanceDataEnhancerService.sectorsMapping, + name: sector + }), weight: weight as number }; }); @@ -331,46 +349,4 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { return { assetClass, assetSubClass }; } - - private parseSector(aString: string) { - let sector = UNKNOWN_KEY; - - switch (aString) { - case 'basic_materials': - sector = 'Basic Materials'; - break; - case 'communication_services': - sector = 'Communication Services'; - break; - case 'consumer_cyclical': - sector = 'Consumer Cyclical'; - break; - case 'consumer_defensive': - sector = 'Consumer Staples'; - break; - case 'energy': - sector = 'Energy'; - break; - case 'financial_services': - sector = 'Financial Services'; - break; - case 'healthcare': - sector = 'Healthcare'; - break; - case 'industrials': - sector = 'Industrials'; - break; - case 'realestate': - sector = 'Real Estate'; - break; - case 'technology': - sector = 'Technology'; - break; - case 'utilities': - sector = 'Utilities'; - break; - } - - return sector; - } } diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index 560a00164..fc08c3680 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -246,6 +246,8 @@ export class GfAssetProfileDialogComponent implements OnInit { [name: string]: { name: string; value: number }; }; + protected readonly translate = translate; + protected user: User; private benchmarks: Partial[]; @@ -381,7 +383,7 @@ export class GfAssetProfileDialogComponent implements OnInit { ) { for (const { name, weight } of this.assetProfile.sectors) { this.sectors[name] = { - name, + name: translate(name), value: weight }; } 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 ddcf96b3b..474fff3ca 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 @@ -258,7 +258,7 @@ i18n size="medium" [locale]="data.locale" - [value]="assetProfile?.sectors[0].name" + [value]="translate(assetProfile?.sectors[0].name)" >Sector 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 8c42e37ea..e745decd0 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 @@ -157,6 +157,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { public SymbolProfile: EnhancedSymbolProfile; public tags: Tag[]; public tagsAvailable: Tag[]; + public translate = translate; public user: User; public value: number; @@ -442,7 +443,7 @@ export class GfHoldingDetailDialogComponent implements OnInit { if (SymbolProfile?.sectors?.length > 0) { for (const sector of SymbolProfile.sectors) { this.sectors[sector.name] = { - name: sector.name, + name: translate(sector.name), value: sector.weight }; } 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 4b04a0986..478a8e5a3 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 @@ -262,7 +262,7 @@ i18n size="medium" [locale]="data.locale" - [value]="SymbolProfile.sectors[0].name" + [value]="translate(SymbolProfile.sectors[0].name)" >Sector diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index f48b551bb..792f32bf5 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -442,7 +442,7 @@ export class GfAllocationsPageComponent implements OnInit { : position.valueInPercentage); } else { this.sectors[name] = { - name, + name: translate(name), value: weight * (isNumber(position.valueInBaseCurrency) diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts index f52639db6..52a7864ac 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -9,6 +9,7 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { Market } from '@ghostfolio/common/types'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table/activities-table.component'; import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table/holdings-table.component'; +import { translate } from '@ghostfolio/ui/i18n'; import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.component'; import { DataService } from '@ghostfolio/ui/services'; import { GfValueComponent } from '@ghostfolio/ui/value'; @@ -232,7 +233,7 @@ export class GfPublicPageComponent implements OnInit { weight * (position.valueInBaseCurrency ?? 0); } else { this.sectors[name] = { - name, + name: translate(name), value: weight * (this.publicPortfolioDetails.holdings[symbol] diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 28d902d71..5f2dd9a1c 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -282,6 +282,21 @@ export const REPLACE_NAME_PARTS = [ 'Xtrackers (IE) Plc -' ]; +export const SECTORS = [ + 'Basic Materials', + 'Communication Services', + 'Consumer Cyclical', + 'Consumer Defensive', + 'Energy', + 'Financial Services', + 'Healthcare', + 'Industrials', + 'Other', + 'Real Estate', + 'Technology', + 'Utilities' +] as const; + export const STORYBOOK_PATH = '/development/storybook'; export const SUPPORTED_LANGUAGE_CODES = [ diff --git a/libs/common/src/lib/types/index.ts b/libs/common/src/lib/types/index.ts index 781e50c55..b6e513a51 100644 --- a/libs/common/src/lib/types/index.ts +++ b/libs/common/src/lib/types/index.ts @@ -17,6 +17,7 @@ import type { MarketState } from './market-state.type'; import type { Market } from './market.type'; import type { OrderWithAccount } from './order-with-account.type'; import type { RequestWithUser } from './request-with-user.type'; +import type { SectorName } from './sector-name.type'; import type { SubscriptionOfferKey } from './subscription-offer-key.type'; import type { UserWithSettings } from './user-with-settings.type'; import type { ViewMode } from './view-mode.type'; @@ -41,6 +42,7 @@ export type { MarketState, OrderWithAccount, RequestWithUser, + SectorName, SubscriptionOfferKey, UserWithSettings, ViewMode diff --git a/libs/common/src/lib/types/sector-name.type.ts b/libs/common/src/lib/types/sector-name.type.ts new file mode 100644 index 000000000..0d9ea9cee --- /dev/null +++ b/libs/common/src/lib/types/sector-name.type.ts @@ -0,0 +1,3 @@ +import type { SECTORS } from '../config'; + +export type SectorName = (typeof SECTORS)[number]; diff --git a/libs/ui/src/lib/i18n.ts b/libs/ui/src/lib/i18n.ts index c7d8b7c8b..f6f1e8ff9 100644 --- a/libs/ui/src/lib/i18n.ts +++ b/libs/ui/src/lib/i18n.ts @@ -1,3 +1,5 @@ +import type { SectorName } from '@ghostfolio/common/types'; + import '@angular/localize/init'; const locales = { @@ -107,8 +109,22 @@ const locales = { EXTREME_GREED: $localize`Extreme Greed`, FEAR: $localize`Fear`, GREED: $localize`Greed`, - NEUTRAL: $localize`Neutral` -}; + NEUTRAL: $localize`Neutral`, + + // Sectors + 'Basic Materials': $localize`Basic Materials`, + 'Communication Services': $localize`Communication Services`, + 'Consumer Cyclical': $localize`Consumer Cyclical`, + 'Consumer Defensive': $localize`Consumer Defensive`, + Energy: $localize`Energy`, + 'Financial Services': $localize`Financial Services`, + Healthcare: $localize`Healthcare`, + Industrials: $localize`Industrials`, + Other: $localize`Other`, + 'Real Estate': $localize`Real Estate`, + Technology: $localize`Technology`, + Utilities: $localize`Utilities` +} satisfies Record & Record; export function translate(aKey: string): string { return locales[aKey] ?? aKey;