Browse Source

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
commit
3a4b0ce304
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 28
      apps/api/src/helper/sector.helper.ts
  3. 15
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  4. 66
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  5. 4
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  6. 2
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  7. 3
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  8. 2
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  9. 2
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  10. 3
      apps/client/src/app/pages/public/public-page.component.ts
  11. 15
      libs/common/src/lib/config.ts
  12. 2
      libs/common/src/lib/types/index.ts
  13. 3
      libs/common/src/lib/types/sector-name.type.ts
  14. 20
      libs/ui/src/lib/i18n.ts

2
CHANGELOG.md

@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Harmonized the sector names across the data providers
- Localized the sector names
- Centralized the asset profile override logic for manual adjustments - 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 - 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 - Prevented the deletion of asset profiles that are currently in use

28
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<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';
}

15
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 { 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 { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service'; import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { Holding } from '@ghostfolio/common/interfaces'; import { Holding } from '@ghostfolio/common/interfaces';
import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { SectorName } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
@ -17,11 +19,13 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
USA: 'United States' USA: 'United States'
}; };
private static holdingsWeightTreshold = 0.85; private static holdingsWeightTreshold = 0.85;
private static sectorsMapping = { private static sectorsMapping: Record<string, SectorName> = {
'Consumer Discretionary': 'Consumer Cyclical', 'Consumer Discretionary': 'Consumer Cyclical',
'Consumer Defensive': 'Consumer Staples', 'Consumer Staples': 'Consumer Defensive',
Financials: 'Financial Services',
'Health Care': 'Healthcare', 'Health Care': 'Healthcare',
'Information Technology': 'Technology' 'Information Technology': 'Technology',
Materials: 'Basic Materials'
}; };
private readonly logger = new Logger(TrackinsightDataEnhancerService.name); private readonly logger = new Logger(TrackinsightDataEnhancerService.name);
@ -155,7 +159,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
holdings?.sectors ?? {} holdings?.sectors ?? {}
)) { )) {
response.sectors.push({ response.sectors.push({
name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name, name: getSectorName({
name,
aliases: TrackinsightDataEnhancerService.sectorsMapping
}),
weight: value.weight weight: value.weight
}); });
} }

66
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 { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { AssetProfileDelistedError } from '@ghostfolio/api/services/data-provider/errors/asset-profile-delisted.error'; 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 { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
REPLACE_NAME_PARTS, REPLACE_NAME_PARTS
UNKNOWN_KEY
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { isCurrency } from '@ghostfolio/common/helper'; import { isCurrency } from '@ghostfolio/common/helper';
import { SectorName } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { import {
@ -23,6 +24,20 @@ import type { Price } from 'yahoo-finance2/esm/src/modules/quoteSummary-iface';
@Injectable() @Injectable()
export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { 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 logger = new Logger(YahooFinanceDataEnhancerService.name);
private readonly yahooFinance = new YahooFinance({ private readonly yahooFinance = new YahooFinance({
@ -224,7 +239,10 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
.flatMap((sectorWeighting) => { .flatMap((sectorWeighting) => {
return Object.entries(sectorWeighting).map(([sector, weight]) => { return Object.entries(sectorWeighting).map(([sector, weight]) => {
return { return {
name: this.parseSector(sector), name: getSectorName({
aliases: YahooFinanceDataEnhancerService.sectorsMapping,
name: sector
}),
weight: weight as number weight: weight as number
}; };
}); });
@ -331,46 +349,4 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
return { assetClass, assetSubClass }; 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;
}
} }

4
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 }; [name: string]: { name: string; value: number };
}; };
protected readonly translate = translate;
protected user: User; protected user: User;
private benchmarks: Partial<SymbolProfile>[]; private benchmarks: Partial<SymbolProfile>[];
@ -381,7 +383,7 @@ export class GfAssetProfileDialogComponent implements OnInit {
) { ) {
for (const { name, weight } of this.assetProfile.sectors) { for (const { name, weight } of this.assetProfile.sectors) {
this.sectors[name] = { this.sectors[name] = {
name, name: translate(name),
value: weight value: weight
}; };
} }

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

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

3
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 SymbolProfile: EnhancedSymbolProfile;
public tags: Tag[]; public tags: Tag[];
public tagsAvailable: Tag[]; public tagsAvailable: Tag[];
public translate = translate;
public user: User; public user: User;
public value: number; public value: number;
@ -442,7 +443,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
if (SymbolProfile?.sectors?.length > 0) { if (SymbolProfile?.sectors?.length > 0) {
for (const sector of SymbolProfile.sectors) { for (const sector of SymbolProfile.sectors) {
this.sectors[sector.name] = { this.sectors[sector.name] = {
name: sector.name, name: translate(sector.name),
value: sector.weight value: sector.weight
}; };
} }

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

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

2
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

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

3
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 { Market } from '@ghostfolio/common/types';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table/activities-table.component'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table/activities-table.component';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table/holdings-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 { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.component';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
@ -232,7 +233,7 @@ export class GfPublicPageComponent implements OnInit {
weight * (position.valueInBaseCurrency ?? 0); weight * (position.valueInBaseCurrency ?? 0);
} else { } else {
this.sectors[name] = { this.sectors[name] = {
name, name: translate(name),
value: value:
weight * weight *
(this.publicPortfolioDetails.holdings[symbol] (this.publicPortfolioDetails.holdings[symbol]

15
libs/common/src/lib/config.ts

@ -282,6 +282,21 @@ export const REPLACE_NAME_PARTS = [
'Xtrackers (IE) Plc -' '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 STORYBOOK_PATH = '/development/storybook';
export const SUPPORTED_LANGUAGE_CODES = [ export const SUPPORTED_LANGUAGE_CODES = [

2
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 { Market } from './market.type';
import type { OrderWithAccount } from './order-with-account.type'; import type { OrderWithAccount } from './order-with-account.type';
import type { RequestWithUser } from './request-with-user.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 { SubscriptionOfferKey } from './subscription-offer-key.type';
import type { UserWithSettings } from './user-with-settings.type'; import type { UserWithSettings } from './user-with-settings.type';
import type { ViewMode } from './view-mode.type'; import type { ViewMode } from './view-mode.type';
@ -41,6 +42,7 @@ export type {
MarketState, MarketState,
OrderWithAccount, OrderWithAccount,
RequestWithUser, RequestWithUser,
SectorName,
SubscriptionOfferKey, SubscriptionOfferKey,
UserWithSettings, UserWithSettings,
ViewMode ViewMode

3
libs/common/src/lib/types/sector-name.type.ts

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

20
libs/ui/src/lib/i18n.ts

@ -1,3 +1,5 @@
import type { SectorName } from '@ghostfolio/common/types';
import '@angular/localize/init'; import '@angular/localize/init';
const locales = { const locales = {
@ -107,8 +109,22 @@ const locales = {
EXTREME_GREED: $localize`Extreme Greed`, EXTREME_GREED: $localize`Extreme Greed`,
FEAR: $localize`Fear`, FEAR: $localize`Fear`,
GREED: $localize`Greed`, 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 { export function translate(aKey: string): string {
return locales[aKey] ?? aKey; return locales[aKey] ?? aKey;

Loading…
Cancel
Save