Browse Source

Task/centralize asset profile override logic (#6991)

* Centralize asset profile logic

* Update changelog
pull/6992/head^2
Thomas Kaul 3 days ago
committed by GitHub
parent
commit
be874f62e3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 110
      apps/api/src/app/admin/admin.service.ts
  3. 29
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts
  4. 60
      apps/api/src/services/symbol-profile/symbol-profile.service.ts
  5. 45
      libs/common/src/lib/helper.ts

2
CHANGELOG.md

@ -13,11 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Centralized the asset profile override logic for manual adjustments
- Refactored the backend logging to use the instance-based `Logger`
- Improved the language localization for Ukrainian (`uk`)
### Fixed
- Fixed an issue where the asset profile override (asset class and asset sub class) was not applied to the data enhancers when gathering asset profiles
- Fixed a layout issue in the asset profile dialog of the admin control by truncating long titles
## 3.7.0 - 2026-06-02

110
apps/api/src/app/admin/admin.service.ts

@ -14,6 +14,7 @@ import {
PROPERTY_IS_USER_SIGNUP_ENABLED
} from '@ghostfolio/common/config';
import {
applyAssetProfileOverrides,
getAssetProfileIdentifier,
getCurrencyFromSymbol,
isCurrency
@ -29,7 +30,6 @@ import {
EnhancedSymbolProfile,
Filter
} from '@ghostfolio/common/interfaces';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { MarketDataPreset } from '@ghostfolio/common/types';
import {
@ -349,87 +349,61 @@ export class AdminService {
}
let marketData: AdminMarketDataItem[] = await Promise.all(
assetProfiles.map(
async ({
assetProfiles.map(async (assetProfile) => {
const {
_count,
activities,
assetClass,
assetSubClass,
comment,
countries,
currency,
dataSource,
id,
isActive,
isUsedByUsersWithSubscription,
name,
sectors,
symbol,
SymbolProfileOverrides
}) => {
let countriesCount = countries ? Object.keys(countries).length : 0;
symbol
} = assetProfile;
const lastMarketPrice = lastMarketPriceMap.get(
getAssetProfileIdentifier({ dataSource, symbol })
const { assetClass, assetSubClass, countries, name, sectors } =
applyAssetProfileOverrides(
assetProfile,
assetProfile.SymbolProfileOverrides
);
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
let sectorsCount = sectors ? Object.keys(sectors).length : 0;
if (SymbolProfileOverrides) {
assetClass = SymbolProfileOverrides.assetClass ?? assetClass;
assetSubClass =
SymbolProfileOverrides.assetSubClass ?? assetSubClass;
if (
(SymbolProfileOverrides.countries as unknown as Prisma.JsonArray)
?.length > 0
) {
countriesCount = (
SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
).length;
}
const countriesCount = countries ? Object.keys(countries).length : 0;
name = SymbolProfileOverrides.name ?? name;
const lastMarketPrice = lastMarketPriceMap.get(
getAssetProfileIdentifier({ dataSource, symbol })
);
if (
(SymbolProfileOverrides.sectors as unknown as Sector[])?.length >
0
) {
sectorsCount = (
SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray
).length;
}
}
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
return {
assetClass,
assetSubClass,
comment,
countriesCount,
currency,
dataSource,
id,
isActive,
lastMarketPrice,
marketDataItemCount,
name,
sectorsCount,
symbol,
activitiesCount: _count.activities,
date: activities?.[0]?.date,
isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription,
watchedByCount: _count.watchedBy
};
}
)
const sectorsCount = sectors ? Object.keys(sectors).length : 0;
return {
assetClass,
assetSubClass,
comment,
countriesCount,
currency,
dataSource,
id,
isActive,
lastMarketPrice,
marketDataItemCount,
name,
sectorsCount,
symbol,
activitiesCount: _count.activities,
date: activities?.[0]?.date,
isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription,
watchedByCount: _count.watchedBy
};
})
);
if (presetId) {

29
apps/api/src/services/queues/data-gathering/data-gathering.service.ts

@ -178,14 +178,27 @@ export class DataGatheringService {
);
for (const [symbol, assetProfile] of Object.entries(assetProfiles)) {
const symbolMapping = symbolProfiles.find((symbolProfile) => {
return symbolProfile.symbol === symbol;
})?.symbolMapping;
const symbolProfile = symbolProfiles.find(
({ symbol: symbolProfileSymbol }) => {
return symbolProfileSymbol === symbol;
}
);
const symbolMapping = symbolProfile?.symbolMapping;
let enhancedAssetProfile = symbolProfile
? {
...assetProfile,
assetClass: symbolProfile.assetClass ?? assetProfile.assetClass,
assetSubClass:
symbolProfile.assetSubClass ?? assetProfile.assetSubClass
}
: assetProfile;
for (const dataEnhancer of this.dataEnhancers) {
try {
assetProfiles[symbol] = await dataEnhancer.enhance({
response: assetProfile,
enhancedAssetProfile = await dataEnhancer.enhance({
response: enhancedAssetProfile,
symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol
});
} catch (error) {
@ -198,9 +211,9 @@ export class DataGatheringService {
}
}
const { assetClass, assetSubClass } = assetProfile;
const {
assetClass,
assetSubClass,
countries,
currency,
cusip,
@ -213,7 +226,7 @@ export class DataGatheringService {
name,
sectors,
url
} = assetProfile;
} = enhancedAssetProfile;
try {
await this.prismaService.symbolProfile.upsert({

60
apps/api/src/services/symbol-profile/symbol-profile.service.ts

@ -1,5 +1,6 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { applyAssetProfileOverrides } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
EnhancedSymbolProfile,
@ -192,21 +193,28 @@ export class SymbolProfileService {
})[]
): EnhancedSymbolProfile[] {
return symbolProfiles.map((symbolProfile) => {
const symbolProfileWithOverrides = applyAssetProfileOverrides(
symbolProfile,
symbolProfile.SymbolProfileOverrides
);
const item = {
...symbolProfile,
...symbolProfileWithOverrides,
activitiesCount: 0,
countries: this.getCountries(
symbolProfile?.countries as unknown as Prisma.JsonArray
symbolProfileWithOverrides?.countries as unknown as Prisma.JsonArray
),
dateOfFirstActivity: undefined as Date,
holdings: this.getHoldings(
symbolProfile?.holdings as unknown as Prisma.JsonArray
symbolProfileWithOverrides?.holdings as unknown as Prisma.JsonArray
),
scraperConfiguration: this.getScraperConfiguration(
symbolProfileWithOverrides
),
scraperConfiguration: this.getScraperConfiguration(symbolProfile),
sectors: this.getSectors(
symbolProfile?.sectors as unknown as Prisma.JsonArray
symbolProfileWithOverrides?.sectors as unknown as Prisma.JsonArray
),
symbolMapping: this.getSymbolMapping(symbolProfile),
symbolMapping: this.getSymbolMapping(symbolProfileWithOverrides),
watchedByCount: 0
};
@ -217,45 +225,7 @@ export class SymbolProfileService {
item.dateOfFirstActivity = symbolProfile.activities?.[0]?.date;
delete item.activities;
if (item.SymbolProfileOverrides) {
item.assetClass =
item.SymbolProfileOverrides.assetClass ?? item.assetClass;
item.assetSubClass =
item.SymbolProfileOverrides.assetSubClass ?? item.assetSubClass;
if (
(item.SymbolProfileOverrides.countries as unknown as Prisma.JsonArray)
?.length > 0
) {
item.countries = this.getCountries(
item.SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
);
}
if (
(item.SymbolProfileOverrides.holdings as unknown as Holding[])
?.length > 0
) {
item.holdings = this.getHoldings(
item.SymbolProfileOverrides.holdings as unknown as Prisma.JsonArray
);
}
item.name = item.SymbolProfileOverrides.name ?? item.name;
if (
(item.SymbolProfileOverrides.sectors as unknown as Sector[])?.length >
0
) {
item.sectors = this.getSectors(
item.SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray
);
}
item.url = item.SymbolProfileOverrides.url ?? item.url;
delete item.SymbolProfileOverrides;
}
delete item.SymbolProfileOverrides;
return item;
});

45
libs/common/src/lib/helper.ts

@ -1,5 +1,12 @@
import { NumberParser } from '@internationalized/number';
import { Type as ActivityType, DataSource, MarketData } from '@prisma/client';
import {
Type as ActivityType,
DataSource,
MarketData,
Prisma,
SymbolProfile,
SymbolProfileOverrides
} from '@prisma/client';
import { Big } from 'big.js';
import { isISO4217CurrencyCode } from 'class-validator';
import {
@ -47,6 +54,42 @@ export const DATE_FORMAT = 'yyyy-MM-dd';
export const DATE_FORMAT_MONTHLY = 'MMMM yyyy';
export const DATE_FORMAT_YEARLY = 'yyyy';
export function applyAssetProfileOverrides<T extends Partial<SymbolProfile>>(
assetProfile: T,
assetProfileOverrides: SymbolProfileOverrides | null
): T {
if (!assetProfileOverrides) {
return assetProfile;
}
const assetProfileWithOverrides = { ...assetProfile } as T;
assetProfileWithOverrides.assetClass =
assetProfileOverrides.assetClass ?? assetProfile.assetClass;
assetProfileWithOverrides.assetSubClass =
assetProfileOverrides.assetSubClass ?? assetProfile.assetSubClass;
if ((assetProfileOverrides.countries as Prisma.JsonArray)?.length > 0) {
assetProfileWithOverrides.countries = assetProfileOverrides.countries;
}
if ((assetProfileOverrides.holdings as Prisma.JsonArray)?.length > 0) {
assetProfileWithOverrides.holdings = assetProfileOverrides.holdings;
}
assetProfileWithOverrides.name =
assetProfileOverrides.name ?? assetProfile.name;
if ((assetProfileOverrides.sectors as Prisma.JsonArray)?.length > 0) {
assetProfileWithOverrides.sectors = assetProfileOverrides.sectors;
}
assetProfileWithOverrides.url = assetProfileOverrides.url ?? assetProfile.url;
return assetProfileWithOverrides;
}
export function calculateBenchmarkTrend({
days,
historicalData

Loading…
Cancel
Save