Task/harmonize sector names accross data providers (#6994)

* Harmonize sector names

* Update changelog
pull/6902/head
Thomas Kaul 3 days ago committed by GitHub
parent bf7409ec20
commit 3a4b0ce304
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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 panels users section
- Prevented the deletion of asset profiles that are currently in use

@ -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<string, SectorName>;
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';
}

@ -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<string, SectorName> = {
'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
});
}

@ -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<string, SectorName> = {
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;
}
}

@ -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<SymbolProfile>[];
@ -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
};
}

@ -258,7 +258,7 @@
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.sectors[0].name"
[value]="translate(assetProfile?.sectors[0].name)"
>Sector</gf-value
>
</div>

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

@ -262,7 +262,7 @@
i18n
size="medium"
[locale]="data.locale"
[value]="SymbolProfile.sectors[0].name"
[value]="translate(SymbolProfile.sectors[0].name)"
>Sector</gf-value
>
</div>

@ -442,7 +442,7 @@ export class GfAllocationsPageComponent implements OnInit {
: position.valueInPercentage);
} else {
this.sectors[name] = {
name,
name: translate(name),
value:
weight *
(isNumber(position.valueInBaseCurrency)

@ -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]

@ -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 = [

@ -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

@ -0,0 +1,3 @@
import type { SECTORS } from '../config';
export type SectorName = (typeof SECTORS)[number];

@ -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<SectorName, string> & Record<string, string>;
export function translate(aKey: string): string {
return locales[aKey] ?? aKey;

Loading…
Cancel
Save