Browse Source

Centralize asset profile logic

pull/6991/head
Thomas Kaul 3 days ago
parent
commit
0057641c74
  1. 110
      apps/api/src/app/admin/admin.service.ts
  2. 29
      apps/api/src/services/queues/data-gathering/data-gathering.service.ts
  3. 60
      apps/api/src/services/symbol-profile/symbol-profile.service.ts
  4. 46
      libs/common/src/lib/helper.ts

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

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

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

@ -176,9 +176,28 @@ export class DataGatheringService {
); );
for (const [symbol, assetProfile] of Object.entries(assetProfiles)) { for (const [symbol, assetProfile] of Object.entries(assetProfiles)) {
const symbolMapping = symbolProfiles.find((symbolProfile) => { const symbolProfile = symbolProfiles.find(
return symbolProfile.symbol === symbol; ({ symbol: symbolProfileSymbol }) => {
})?.symbolMapping; return symbolProfileSymbol === symbol;
}
);
const symbolMapping = symbolProfile?.symbolMapping;
// Persist the data provider’s classification in the base asset profile;
// the asset profile override stays a separate layer applied at read time
const {
assetClass: assetClassFromDataProvider,
assetSubClass: assetSubClassFromDataProvider
} = assetProfile;
// Apply the asset profile override so the data enhancers treat the asset
// profile correctly (e.g. enrich an ETF that the data provider classifies
// as a stock)
if (symbolProfile) {
assetProfile.assetClass = symbolProfile.assetClass;
assetProfile.assetSubClass = symbolProfile.assetSubClass;
}
for (const dataEnhancer of this.dataEnhancers) { for (const dataEnhancer of this.dataEnhancers) {
try { try {
@ -197,6 +216,10 @@ export class DataGatheringService {
} }
} }
// Restore the data provider’s classification before persisting
assetProfile.assetClass = assetClassFromDataProvider;
assetProfile.assetSubClass = assetSubClassFromDataProvider;
const { const {
assetClass, assetClass,
assetSubClass, assetSubClass,

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 { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { applySymbolProfileOverrides } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
EnhancedSymbolProfile, EnhancedSymbolProfile,
@ -192,21 +193,28 @@ export class SymbolProfileService {
})[] })[]
): EnhancedSymbolProfile[] { ): EnhancedSymbolProfile[] {
return symbolProfiles.map((symbolProfile) => { return symbolProfiles.map((symbolProfile) => {
const symbolProfileWithOverrides = applySymbolProfileOverrides(
symbolProfile,
symbolProfile.SymbolProfileOverrides
);
const item = { const item = {
...symbolProfile, ...symbolProfileWithOverrides,
activitiesCount: 0, activitiesCount: 0,
countries: this.getCountries( countries: this.getCountries(
symbolProfile?.countries as unknown as Prisma.JsonArray symbolProfileWithOverrides?.countries as unknown as Prisma.JsonArray
), ),
dateOfFirstActivity: undefined as Date, dateOfFirstActivity: undefined as Date,
holdings: this.getHoldings( 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( 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 watchedByCount: 0
}; };
@ -217,45 +225,7 @@ export class SymbolProfileService {
item.dateOfFirstActivity = symbolProfile.activities?.[0]?.date; item.dateOfFirstActivity = symbolProfile.activities?.[0]?.date;
delete item.activities; delete item.activities;
if (item.SymbolProfileOverrides) { delete 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;
}
return item; return item;
}); });

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

@ -1,5 +1,12 @@
import { NumberParser } from '@internationalized/number'; 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 { Big } from 'big.js';
import { isISO4217CurrencyCode } from 'class-validator'; import { isISO4217CurrencyCode } from 'class-validator';
import { import {
@ -47,6 +54,43 @@ export const DATE_FORMAT = 'yyyy-MM-dd';
export const DATE_FORMAT_MONTHLY = 'MMMM yyyy'; export const DATE_FORMAT_MONTHLY = 'MMMM yyyy';
export const DATE_FORMAT_YEARLY = 'yyyy'; export const DATE_FORMAT_YEARLY = 'yyyy';
export function applySymbolProfileOverrides<T extends Partial<SymbolProfile>>(
symbolProfile: T,
symbolProfileOverrides: SymbolProfileOverrides | null
): T {
if (!symbolProfileOverrides) {
return symbolProfile;
}
const symbolProfileWithOverrides = { ...symbolProfile } as T;
symbolProfileWithOverrides.assetClass =
symbolProfileOverrides.assetClass ?? symbolProfile.assetClass;
symbolProfileWithOverrides.assetSubClass =
symbolProfileOverrides.assetSubClass ?? symbolProfile.assetSubClass;
if ((symbolProfileOverrides.countries as Prisma.JsonArray)?.length > 0) {
symbolProfileWithOverrides.countries = symbolProfileOverrides.countries;
}
if ((symbolProfileOverrides.holdings as Prisma.JsonArray)?.length > 0) {
symbolProfileWithOverrides.holdings = symbolProfileOverrides.holdings;
}
symbolProfileWithOverrides.name =
symbolProfileOverrides.name ?? symbolProfile.name;
if ((symbolProfileOverrides.sectors as Prisma.JsonArray)?.length > 0) {
symbolProfileWithOverrides.sectors = symbolProfileOverrides.sectors;
}
symbolProfileWithOverrides.url =
symbolProfileOverrides.url ?? symbolProfile.url;
return symbolProfileWithOverrides;
}
export function calculateBenchmarkTrend({ export function calculateBenchmarkTrend({
days, days,
historicalData historicalData

Loading…
Cancel
Save