Browse Source

Task/improve sector mapping of Trackinsight data enhancer (#8367)

* Improve sector mapping

* Update changelog
pull/8370/head
Thomas Kaul 4 days ago
committed by GitHub
parent
commit
33e0ae08d5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 96
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.spec.ts
  3. 32
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Extended the sectors mapping in the data enhancer for asset profile data via _Trackinsight_
- Improved the logging of the `web_fetch` tool in the `FetchService`
- Improved the validation of the `WEB_FETCH_ROUTES` setting in the `FetchService`
- Improved the validation of the response of the `web_fetch` tool in the `FetchService`

96
apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.spec.ts

@ -327,6 +327,102 @@ describe('TrackinsightDataEnhancerService', () => {
expect(response.holdings).toEqual([{ name: 'NVIDIA Corp.', weight: 0.08 }]);
});
it.each([
['Business Services', 'Industrials'],
['Consumer Cyclicals', 'Consumer Cyclical'],
['Consumer Discretionary', 'Consumer Cyclical'],
['Consumer Non-Cyclicals', 'Consumer Defensive'],
['Consumer Services', 'Consumer Cyclical'],
['Consumer Staples', 'Consumer Defensive'],
['Finance', 'Financial Services'],
['Financials', 'Financial Services'],
['Health Care', 'Healthcare'],
['Information Technology', 'Technology'],
['Materials', 'Basic Materials'],
['Non-Energy Materials', 'Basic Materials'],
['Telecommunications', 'Communication Services']
])('should map the sector %s to %s', async (name, expected) => {
const warn = jest.spyOn(Logger.prototype, 'warn').mockImplementation();
mockResponses({
holdings: Response.json({
...HOLDINGS,
sectors: { [name]: { weight: 0.5 } }
})
});
const response = await enhance();
expect(response.sectors).toEqual([{ name: expected, weight: 0.5 }]);
expect(warn).not.toHaveBeenCalled();
warn.mockRestore();
});
it('should merge the sectors with the same mapping', async () => {
mockResponses({
holdings: Response.json({
...HOLDINGS,
sectors: {
'Business Services': { weight: 0.25 },
'Consumer Cyclicals': { weight: 0.125 },
'Consumer Services': { weight: 0.125 },
Industrials: { weight: 0.5 }
}
})
});
const response = await enhance();
expect(response.sectors).toEqual([
{ name: 'Industrials', weight: 0.75 },
{ name: 'Consumer Cyclical', weight: 0.25 }
]);
});
it('should merge the sectors without floating point noise', async () => {
mockResponses({
holdings: Response.json({
...HOLDINGS,
sectors: {
'Consumer Cyclicals': { weight: 0.1 },
'Consumer Services': { weight: 0.2 }
}
})
});
const response = await enhance();
expect(response.sectors).toEqual([
{ name: 'Consumer Cyclical', weight: 0.3 }
]);
});
it('should merge the sectors without a mapping', async () => {
const warn = jest.spyOn(Logger.prototype, 'warn').mockImplementation();
mockResponses({
holdings: Response.json({
...HOLDINGS,
sectors: {
'New Sector A': { weight: 0.25 },
'New Sector B': { weight: 0.25 },
Technology: { weight: 0.5 }
}
})
});
const response = await enhance();
expect(response.sectors).toEqual([
{ name: 'Other', weight: 0.5 },
{ name: 'Technology', weight: 0.5 }
]);
expect(warn).toHaveBeenCalledTimes(2);
warn.mockRestore();
});
it('should accept floating point noise in a weight', async () => {
mockResponses({
holdings: Response.json({

32
apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts

@ -10,6 +10,7 @@ import { SectorName } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client';
import { Big } from 'big.js';
import { isPlainObject } from 'lodash-es';
@Injectable()
@ -29,12 +30,19 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
private static holdingsWeightTreshold = 0.85;
private static sectorsMapping: Record<string, SectorName> = {
'Business Services': 'Industrials',
'Consumer Cyclicals': 'Consumer Cyclical',
'Consumer Discretionary': 'Consumer Cyclical',
'Consumer Non-Cyclicals': 'Consumer Defensive',
'Consumer Services': 'Consumer Cyclical',
'Consumer Staples': 'Consumer Defensive',
Finance: 'Financial Services',
Financials: 'Financial Services',
'Health Care': 'Healthcare',
'Information Technology': 'Technology',
Materials: 'Basic Materials'
Materials: 'Basic Materials',
'Non-Energy Materials': 'Basic Materials',
Telecommunications: 'Communication Services'
};
// Allows for floating point noise in the sums of Trackinsight
@ -163,17 +171,25 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
!response.sectors ||
(response.sectors as unknown as Sector[]).length === 0
) {
response.sectors = [];
// Several names of Trackinsight can map to the same sector
const weightsBySector: Record<string, number> = {};
for (const [name, value] of Object.entries<any>(holdings.sectors ?? {})) {
response.sectors.push({
name: getSectorName({
name,
aliases: TrackinsightDataEnhancerService.sectorsMapping
}),
weight: value.weight
const sectorName = getSectorName({
name,
aliases: TrackinsightDataEnhancerService.sectorsMapping
});
weightsBySector[sectorName] = new Big(weightsBySector[sectorName] ?? 0)
.plus(value.weight)
.toNumber();
}
response.sectors = Object.entries(weightsBySector).map(
([name, weight]) => {
return { name, weight };
}
);
}
return Promise.resolve(response);

Loading…
Cancel
Save