Browse Source

Feature/refactor unique asset type to asset profile identifier (#3636)

* Refactoring
pull/3637/head
Thomas Kaul 8 months ago
committed by GitHub
parent
commit
41f5801b5e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 59
      apps/api/src/app/admin/admin.service.ts
  2. 8
      apps/api/src/app/benchmark/benchmark.controller.ts
  3. 15
      apps/api/src/app/benchmark/benchmark.service.ts
  4. 4
      apps/api/src/app/import/import.service.ts
  5. 4
      apps/api/src/app/logo/logo.service.ts
  6. 15
      apps/api/src/app/order/order.service.ts
  7. 7
      apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
  8. 22
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  9. 7
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
  10. 16
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  11. 15
      apps/api/src/app/portfolio/current-rate.service.ts
  12. 4
      apps/api/src/app/portfolio/interfaces/get-value-object.interface.ts
  13. 4
      apps/api/src/app/redis-cache/redis-cache.service.ts
  14. 7
      apps/api/src/app/symbol/interfaces/symbol-item.interface.ts
  15. 6
      apps/api/src/app/symbol/symbol.service.ts
  16. 4
      apps/api/src/services/data-gathering/data-gathering.processor.ts
  17. 43
      apps/api/src/services/data-gathering/data-gathering.service.ts
  18. 14
      apps/api/src/services/data-provider/data-provider.service.ts
  19. 24
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  20. 7
      apps/api/src/services/interfaces/interfaces.ts
  21. 16
      apps/api/src/services/market-data/market-data.service.ts
  22. 12
      apps/api/src/services/symbol-profile/symbol-profile.service.ts
  23. 16
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  24. 18
      apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
  25. 15
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  26. 4
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  27. 4
      apps/client/src/app/components/home-overview/home-overview.component.ts
  28. 19
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  29. 8
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html
  30. 6
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  31. 19
      apps/client/src/app/services/admin.service.ts
  32. 14
      apps/client/src/app/services/data.service.ts
  33. 9
      libs/common/src/lib/helper.ts
  34. 6
      libs/common/src/lib/interfaces/admin-data.interface.ts
  35. 2
      libs/common/src/lib/interfaces/asset-profile-identifier.interface.ts
  36. 4
      libs/common/src/lib/interfaces/index.ts
  37. 4
      libs/common/src/lib/interfaces/responses/errors.interface.ts
  38. 4
      libs/common/src/lib/models/portfolio-snapshot.ts
  39. 6
      libs/ui/src/lib/activities-table/activities-table.component.ts
  40. 4
      libs/ui/src/lib/assistant/interfaces/interfaces.ts
  41. 13
      libs/ui/src/lib/benchmark/benchmark.component.ts
  42. 7
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  43. 7
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  44. 7
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

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

@ -21,9 +21,9 @@ import {
AdminMarketData, AdminMarketData,
AdminMarketDataDetails, AdminMarketDataDetails,
AdminMarketDataItem, AdminMarketDataItem,
AssetProfileIdentifier,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Filter, Filter
UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { MarketDataPreset } from '@ghostfolio/common/types'; import { MarketDataPreset } from '@ghostfolio/common/types';
@ -59,7 +59,9 @@ export class AdminService {
currency, currency,
dataSource, dataSource,
symbol symbol
}: UniqueAsset & { currency?: string }): Promise<SymbolProfile | never> { }: AssetProfileIdentifier & { currency?: string }): Promise<
SymbolProfile | never
> {
try { try {
if (dataSource === 'MANUAL') { if (dataSource === 'MANUAL') {
return this.symbolProfileService.add({ return this.symbolProfileService.add({
@ -96,7 +98,10 @@ export class AdminService {
} }
} }
public async deleteProfileData({ dataSource, symbol }: UniqueAsset) { public async deleteProfileData({
dataSource,
symbol
}: AssetProfileIdentifier) {
await this.marketDataService.deleteMany({ dataSource, symbol }); await this.marketDataService.deleteMany({ dataSource, symbol });
await this.symbolProfileService.delete({ dataSource, symbol }); await this.symbolProfileService.delete({ dataSource, symbol });
} }
@ -325,7 +330,7 @@ export class AdminService {
public async getMarketDataBySymbol({ public async getMarketDataBySymbol({
dataSource, dataSource,
symbol symbol
}: UniqueAsset): Promise<AdminMarketDataDetails> { }: AssetProfileIdentifier): Promise<AdminMarketDataDetails> {
let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0; let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
let currency: EnhancedSymbolProfile['currency'] = '-'; let currency: EnhancedSymbolProfile['currency'] = '-';
let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity']; let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
@ -386,7 +391,7 @@ export class AdminService {
symbol, symbol,
symbolMapping, symbolMapping,
url url
}: Prisma.SymbolProfileUpdateInput & UniqueAsset) { }: AssetProfileIdentifier & Prisma.SymbolProfileUpdateInput) {
const symbolProfileOverrides = { const symbolProfileOverrides = {
assetClass: assetClass as AssetClass, assetClass: assetClass as AssetClass,
assetSubClass: assetSubClass as AssetSubClass, assetSubClass: assetSubClass as AssetSubClass,
@ -394,28 +399,28 @@ export class AdminService {
url: url as string url: url as string
}; };
const updatedSymbolProfile: Prisma.SymbolProfileUpdateInput & UniqueAsset = const updatedSymbolProfile: AssetProfileIdentifier &
{ Prisma.SymbolProfileUpdateInput = {
comment, comment,
countries, countries,
currency, currency,
dataSource, dataSource,
holdings, holdings,
scraperConfiguration, scraperConfiguration,
sectors, sectors,
symbol, symbol,
symbolMapping, symbolMapping,
...(dataSource === 'MANUAL' ...(dataSource === 'MANUAL'
? { assetClass, assetSubClass, name, url } ? { assetClass, assetSubClass, name, url }
: { : {
SymbolProfileOverrides: { SymbolProfileOverrides: {
upsert: { upsert: {
create: symbolProfileOverrides, create: symbolProfileOverrides,
update: symbolProfileOverrides update: symbolProfileOverrides
}
} }
}) }
}; })
};
await this.symbolProfileService.updateSymbolProfile(updatedSymbolProfile); await this.symbolProfileService.updateSymbolProfile(updatedSymbolProfile);

8
apps/api/src/app/benchmark/benchmark.controller.ts

@ -4,9 +4,9 @@ import { getInterval } from '@ghostfolio/api/helper/portfolio.helper';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import type { import type {
AssetProfileIdentifier,
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,
BenchmarkResponse, BenchmarkResponse
UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types'; import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
@ -41,7 +41,9 @@ export class BenchmarkController {
@HasPermission(permissions.accessAdminControl) @HasPermission(permissions.accessAdminControl)
@Post() @Post()
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) { public async addBenchmark(
@Body() { dataSource, symbol }: AssetProfileIdentifier
) {
try { try {
const benchmark = await this.benchmarkService.addBenchmark({ const benchmark = await this.benchmarkService.addBenchmark({
dataSource, dataSource,

15
apps/api/src/app/benchmark/benchmark.service.ts

@ -17,11 +17,11 @@ import {
resetHours resetHours
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier,
Benchmark, Benchmark,
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,
BenchmarkProperty, BenchmarkProperty,
BenchmarkResponse, BenchmarkResponse
UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { BenchmarkTrend } from '@ghostfolio/common/types'; import { BenchmarkTrend } from '@ghostfolio/common/types';
@ -61,7 +61,10 @@ export class BenchmarkService {
return 0; return 0;
} }
public async getBenchmarkTrends({ dataSource, symbol }: UniqueAsset) { public async getBenchmarkTrends({
dataSource,
symbol
}: AssetProfileIdentifier) {
const historicalData = await this.marketDataService.marketDataItems({ const historicalData = await this.marketDataService.marketDataItems({
orderBy: { orderBy: {
date: 'desc' date: 'desc'
@ -228,7 +231,7 @@ export class BenchmarkService {
endDate?: Date; endDate?: Date;
startDate: Date; startDate: Date;
userCurrency: string; userCurrency: string;
} & UniqueAsset): Promise<BenchmarkMarketDataDetails> { } & AssetProfileIdentifier): Promise<BenchmarkMarketDataDetails> {
const marketData: { date: string; value: number }[] = []; const marketData: { date: string; value: number }[] = [];
const days = differenceInDays(endDate, startDate) + 1; const days = differenceInDays(endDate, startDate) + 1;
@ -348,7 +351,7 @@ export class BenchmarkService {
public async addBenchmark({ public async addBenchmark({
dataSource, dataSource,
symbol symbol
}: UniqueAsset): Promise<Partial<SymbolProfile>> { }: AssetProfileIdentifier): Promise<Partial<SymbolProfile>> {
const assetProfile = await this.prismaService.symbolProfile.findFirst({ const assetProfile = await this.prismaService.symbolProfile.findFirst({
where: { where: {
dataSource, dataSource,
@ -385,7 +388,7 @@ export class BenchmarkService {
public async deleteBenchmark({ public async deleteBenchmark({
dataSource, dataSource,
symbol symbol
}: UniqueAsset): Promise<Partial<SymbolProfile>> { }: AssetProfileIdentifier): Promise<Partial<SymbolProfile>> {
const assetProfile = await this.prismaService.symbolProfile.findFirst({ const assetProfile = await this.prismaService.symbolProfile.findFirst({
where: { where: {
dataSource, dataSource,

4
apps/api/src/app/import/import.service.ts

@ -19,7 +19,7 @@ import {
getAssetProfileIdentifier, getAssetProfileIdentifier,
parseDate parseDate
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { import {
AccountWithPlatform, AccountWithPlatform,
OrderWithAccount, OrderWithAccount,
@ -51,7 +51,7 @@ export class ImportService {
dataSource, dataSource,
symbol, symbol,
userCurrency userCurrency
}: UniqueAsset & { userCurrency: string }): Promise<Activity[]> { }: AssetProfileIdentifier & { userCurrency: string }): Promise<Activity[]> {
try { try {
const { firstBuyDate, historicalData, orders } = const { firstBuyDate, historicalData, orders } =
await this.portfolioService.getPosition(dataSource, undefined, symbol); await this.portfolioService.getPosition(dataSource, undefined, symbol);

4
apps/api/src/app/logo/logo.service.ts

@ -1,6 +1,6 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { HttpException, Injectable } from '@nestjs/common'; import { HttpException, Injectable } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
@ -17,7 +17,7 @@ export class LogoService {
public async getLogoByDataSourceAndSymbol({ public async getLogoByDataSourceAndSymbol({
dataSource, dataSource,
symbol symbol
}: UniqueAsset) { }: AssetProfileIdentifier) {
if (!DataSource[dataSource]) { if (!DataSource[dataSource]) {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND), getReasonPhrase(StatusCodes.NOT_FOUND),

15
apps/api/src/app/order/order.service.ts

@ -11,9 +11,9 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Filter, Filter
UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
@ -51,7 +51,7 @@ export class OrderService {
symbol, symbol,
tags, tags,
userId userId
}: { tags: Tag[]; userId: string } & UniqueAsset) { }: { tags: Tag[]; userId: string } & AssetProfileIdentifier) {
const orders = await this.prismaService.order.findMany({ const orders = await this.prismaService.order.findMany({
where: { where: {
userId, userId,
@ -285,7 +285,7 @@ export class OrderService {
return count; return count;
} }
public async getLatestOrder({ dataSource, symbol }: UniqueAsset) { public async getLatestOrder({ dataSource, symbol }: AssetProfileIdentifier) {
return this.prismaService.order.findFirst({ return this.prismaService.order.findFirst({
orderBy: { orderBy: {
date: 'desc' date: 'desc'
@ -464,7 +464,7 @@ export class OrderService {
this.prismaService.order.count({ where }) this.prismaService.order.count({ where })
]); ]);
const uniqueAssets = uniqBy( const assetProfileIdentifiers = uniqBy(
orders.map(({ SymbolProfile }) => { orders.map(({ SymbolProfile }) => {
return { return {
dataSource: SymbolProfile.dataSource, dataSource: SymbolProfile.dataSource,
@ -479,8 +479,9 @@ export class OrderService {
} }
); );
const assetProfiles = const assetProfiles = await this.symbolProfileService.getSymbolProfiles(
await this.symbolProfileService.getSymbolProfiles(uniqueAssets); assetProfileIdentifiers
);
const activities = orders.map((order) => { const activities = orders.map((order) => {
const assetProfile = assetProfiles.find(({ dataSource, symbol }) => { const assetProfile = assetProfiles.find(({ dataSource, symbol }) => {

7
apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts

@ -1,5 +1,8 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces'; import {
AssetProfileIdentifier,
SymbolMetrics
} from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
export class MWRPortfolioCalculator extends PortfolioCalculator { export class MWRPortfolioCalculator extends PortfolioCalculator {
@ -27,7 +30,7 @@ export class MWRPortfolioCalculator extends PortfolioCalculator {
}; };
start: Date; start: Date;
step?: number; step?: number;
} & UniqueAsset): SymbolMetrics { } & AssetProfileIdentifier): SymbolMetrics {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
} }

22
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -19,12 +19,12 @@ import {
resetHours resetHours
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier,
DataProviderInfo, DataProviderInfo,
HistoricalDataItem, HistoricalDataItem,
InvestmentItem, InvestmentItem,
ResponseError, ResponseError,
SymbolMetrics, SymbolMetrics
UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
import { DateRange, GroupBy } from '@ghostfolio/common/types'; import { DateRange, GroupBy } from '@ghostfolio/common/types';
@ -356,15 +356,15 @@ export abstract class PortfolioCalculator {
dataSource: item.dataSource, dataSource: item.dataSource,
fee: item.fee, fee: item.fee,
firstBuyDate: item.firstBuyDate, firstBuyDate: item.firstBuyDate,
grossPerformance: !hasErrors ? grossPerformance ?? null : null, grossPerformance: !hasErrors ? (grossPerformance ?? null) : null,
grossPerformancePercentage: !hasErrors grossPerformancePercentage: !hasErrors
? grossPerformancePercentage ?? null ? (grossPerformancePercentage ?? null)
: null, : null,
grossPerformancePercentageWithCurrencyEffect: !hasErrors grossPerformancePercentageWithCurrencyEffect: !hasErrors
? grossPerformancePercentageWithCurrencyEffect ?? null ? (grossPerformancePercentageWithCurrencyEffect ?? null)
: null, : null,
grossPerformanceWithCurrencyEffect: !hasErrors grossPerformanceWithCurrencyEffect: !hasErrors
? grossPerformanceWithCurrencyEffect ?? null ? (grossPerformanceWithCurrencyEffect ?? null)
: null, : null,
investment: totalInvestment, investment: totalInvestment,
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
@ -372,15 +372,15 @@ export abstract class PortfolioCalculator {
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null, marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null,
marketPriceInBaseCurrency: marketPriceInBaseCurrency:
marketPriceInBaseCurrency?.toNumber() ?? null, marketPriceInBaseCurrency?.toNumber() ?? null,
netPerformance: !hasErrors ? netPerformance ?? null : null, netPerformance: !hasErrors ? (netPerformance ?? null) : null,
netPerformancePercentage: !hasErrors netPerformancePercentage: !hasErrors
? netPerformancePercentage ?? null ? (netPerformancePercentage ?? null)
: null, : null,
netPerformancePercentageWithCurrencyEffect: !hasErrors netPerformancePercentageWithCurrencyEffect: !hasErrors
? netPerformancePercentageWithCurrencyEffect ?? null ? (netPerformancePercentageWithCurrencyEffect ?? null)
: null, : null,
netPerformanceWithCurrencyEffect: !hasErrors netPerformanceWithCurrencyEffect: !hasErrors
? netPerformanceWithCurrencyEffect ?? null ? (netPerformanceWithCurrencyEffect ?? null)
: null, : null,
quantity: item.quantity, quantity: item.quantity,
symbol: item.symbol, symbol: item.symbol,
@ -905,7 +905,7 @@ export abstract class PortfolioCalculator {
}; };
start: Date; start: Date;
step?: number; step?: number;
} & UniqueAsset): SymbolMetrics; } & AssetProfileIdentifier): SymbolMetrics;
public getTransactionPoints() { public getTransactionPoints() {
return this.transactionPoints; return this.transactionPoints;

7
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

@ -2,7 +2,10 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/po
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface'; import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces'; import {
AssetProfileIdentifier,
SymbolMetrics
} from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
@ -151,7 +154,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
}; };
start: Date; start: Date;
step?: number; step?: number;
} & UniqueAsset): SymbolMetrics { } & AssetProfileIdentifier): SymbolMetrics {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const currentValues: { [date: string]: Big } = {}; const currentValues: { [date: string]: Big } = {};
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {}; const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};

16
apps/api/src/app/portfolio/current-rate.service.spec.ts

@ -1,7 +1,7 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
@ -24,32 +24,32 @@ jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => {
}); });
}, },
getRange: ({ getRange: ({
assetProfileIdentifiers,
dateRangeEnd, dateRangeEnd,
dateRangeStart, dateRangeStart
uniqueAssets
}: { }: {
assetProfileIdentifiers: AssetProfileIdentifier[];
dateRangeEnd: Date; dateRangeEnd: Date;
dateRangeStart: Date; dateRangeStart: Date;
uniqueAssets: UniqueAsset[];
}) => { }) => {
return Promise.resolve<MarketData[]>([ return Promise.resolve<MarketData[]>([
{ {
createdAt: dateRangeStart, createdAt: dateRangeStart,
dataSource: uniqueAssets[0].dataSource, dataSource: assetProfileIdentifiers[0].dataSource,
date: dateRangeStart, date: dateRangeStart,
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d', id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
marketPrice: 1841.823902, marketPrice: 1841.823902,
state: 'CLOSE', state: 'CLOSE',
symbol: uniqueAssets[0].symbol symbol: assetProfileIdentifiers[0].symbol
}, },
{ {
createdAt: dateRangeEnd, createdAt: dateRangeEnd,
dataSource: uniqueAssets[0].dataSource, dataSource: assetProfileIdentifiers[0].dataSource,
date: dateRangeEnd, date: dateRangeEnd,
id: '082d6893-df27-4c91-8a5d-092e84315b56', id: '082d6893-df27-4c91-8a5d-092e84315b56',
marketPrice: 1847.839966, marketPrice: 1847.839966,
state: 'CLOSE', state: 'CLOSE',
symbol: uniqueAssets[0].symbol symbol: assetProfileIdentifiers[0].symbol
} }
]); ]);
} }

15
apps/api/src/app/portfolio/current-rate.service.ts

@ -3,9 +3,9 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier,
DataProviderInfo, DataProviderInfo,
ResponseError, ResponseError
UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { RequestWithUser } from '@ghostfolio/common/types';
@ -80,17 +80,16 @@ export class CurrentRateService {
); );
} }
const uniqueAssets: UniqueAsset[] = dataGatheringItems.map( const assetProfileIdentifiers: AssetProfileIdentifier[] =
({ dataSource, symbol }) => { dataGatheringItems.map(({ dataSource, symbol }) => {
return { dataSource, symbol }; return { dataSource, symbol };
} });
);
promises.push( promises.push(
this.marketDataService this.marketDataService
.getRange({ .getRange({
dateQuery, assetProfileIdentifiers,
uniqueAssets dateQuery
}) })
.then((data) => { .then((data) => {
return data.map(({ dataSource, date, marketPrice, symbol }) => { return data.map(({ dataSource, date, marketPrice, symbol }) => {

4
apps/api/src/app/portfolio/interfaces/get-value-object.interface.ts

@ -1,6 +1,6 @@
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
export interface GetValueObject extends UniqueAsset { export interface GetValueObject extends AssetProfileIdentifier {
date: Date; date: Date;
marketPrice: number; marketPrice: number;
} }

4
apps/api/src/app/redis-cache/redis-cache.service.ts

@ -1,6 +1,6 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
@ -28,7 +28,7 @@ export class RedisCacheService {
return `portfolio-snapshot-${userId}`; return `portfolio-snapshot-${userId}`;
} }
public getQuoteKey({ dataSource, symbol }: UniqueAsset) { public getQuoteKey({ dataSource, symbol }: AssetProfileIdentifier) {
return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`; return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`;
} }

7
apps/api/src/app/symbol/interfaces/symbol-item.interface.ts

@ -1,6 +1,9 @@
import { HistoricalDataItem, UniqueAsset } from '@ghostfolio/common/interfaces'; import {
AssetProfileIdentifier,
HistoricalDataItem
} from '@ghostfolio/common/interfaces';
export interface SymbolItem extends UniqueAsset { export interface SymbolItem extends AssetProfileIdentifier {
currency: string; currency: string;
historicalData: HistoricalDataItem[]; historicalData: HistoricalDataItem[];
marketPrice: number; marketPrice: number;

6
apps/api/src/app/symbol/symbol.service.ts

@ -40,13 +40,13 @@ export class SymbolService {
const days = includeHistoricalData; const days = includeHistoricalData;
const marketData = await this.marketDataService.getRange({ const marketData = await this.marketDataService.getRange({
dateQuery: { gte: subDays(new Date(), days) }, assetProfileIdentifiers: [
uniqueAssets: [
{ {
dataSource: dataGatheringItem.dataSource, dataSource: dataGatheringItem.dataSource,
symbol: dataGatheringItem.symbol symbol: dataGatheringItem.symbol
} }
] ],
dateQuery: { gte: subDays(new Date(), days) }
}); });
historicalData = marketData.map(({ date, marketPrice: value }) => { historicalData = marketData.map(({ date, marketPrice: value }) => {

4
apps/api/src/services/data-gathering/data-gathering.processor.ts

@ -7,7 +7,7 @@ import {
GATHER_HISTORICAL_MARKET_DATA_PROCESS GATHER_HISTORICAL_MARKET_DATA_PROCESS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, getStartOfUtcDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, getStartOfUtcDate } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { Process, Processor } from '@nestjs/bull'; import { Process, Processor } from '@nestjs/bull';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
@ -35,7 +35,7 @@ export class DataGatheringProcessor {
) {} ) {}
@Process({ concurrency: 1, name: GATHER_ASSET_PROFILE_PROCESS }) @Process({ concurrency: 1, name: GATHER_ASSET_PROFILE_PROCESS })
public async gatherAssetProfile(job: Job<UniqueAsset>) { public async gatherAssetProfile(job: Job<AssetProfileIdentifier>) {
try { try {
Logger.log( Logger.log(
`Asset profile data gathering has been started for ${job.data.symbol} (${job.data.dataSource})`, `Asset profile data gathering has been started for ${job.data.symbol} (${job.data.dataSource})`,

43
apps/api/src/services/data-gathering/data-gathering.service.ts

@ -20,7 +20,10 @@ import {
getAssetProfileIdentifier, getAssetProfileIdentifier,
resetHours resetHours
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { BenchmarkProperty, UniqueAsset } from '@ghostfolio/common/interfaces'; import {
AssetProfileIdentifier,
BenchmarkProperty
} from '@ghostfolio/common/interfaces';
import { InjectQueue } from '@nestjs/bull'; import { InjectQueue } from '@nestjs/bull';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
@ -91,7 +94,7 @@ export class DataGatheringService {
}); });
} }
public async gatherSymbol({ dataSource, symbol }: UniqueAsset) { public async gatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) {
await this.marketDataService.deleteMany({ dataSource, symbol }); await this.marketDataService.deleteMany({ dataSource, symbol });
const dataGatheringItems = (await this.getSymbolsMax()).filter( const dataGatheringItems = (await this.getSymbolsMax()).filter(
@ -146,23 +149,29 @@ export class DataGatheringService {
} }
} }
public async gatherAssetProfiles(aUniqueAssets?: UniqueAsset[]) { public async gatherAssetProfiles(
let uniqueAssets = aUniqueAssets?.filter((dataGatheringItem) => { aAssetProfileIdentifiers?: AssetProfileIdentifier[]
return dataGatheringItem.dataSource !== 'MANUAL'; ) {
}); let assetProfileIdentifiers = aAssetProfileIdentifiers?.filter(
(dataGatheringItem) => {
return dataGatheringItem.dataSource !== 'MANUAL';
}
);
if (!uniqueAssets) { if (!assetProfileIdentifiers) {
uniqueAssets = await this.getAllAssetProfileIdentifiers(); assetProfileIdentifiers = await this.getAllAssetProfileIdentifiers();
} }
if (uniqueAssets.length <= 0) { if (assetProfileIdentifiers.length <= 0) {
return; return;
} }
const assetProfiles = const assetProfiles = await this.dataProviderService.getAssetProfiles(
await this.dataProviderService.getAssetProfiles(uniqueAssets); assetProfileIdentifiers
const symbolProfiles = );
await this.symbolProfileService.getSymbolProfiles(uniqueAssets); const symbolProfiles = await this.symbolProfileService.getSymbolProfiles(
assetProfileIdentifiers
);
for (const [symbol, assetProfile] of Object.entries(assetProfiles)) { for (const [symbol, assetProfile] of Object.entries(assetProfiles)) {
const symbolMapping = symbolProfiles.find((symbolProfile) => { const symbolMapping = symbolProfiles.find((symbolProfile) => {
@ -248,7 +257,7 @@ export class DataGatheringService {
'DataGatheringService' 'DataGatheringService'
); );
if (uniqueAssets.length === 1) { if (assetProfileIdentifiers.length === 1) {
throw error; throw error;
} }
} }
@ -284,7 +293,9 @@ export class DataGatheringService {
); );
} }
public async getAllAssetProfileIdentifiers(): Promise<UniqueAsset[]> { public async getAllAssetProfileIdentifiers(): Promise<
AssetProfileIdentifier[]
> {
const symbolProfiles = await this.prismaService.symbolProfile.findMany({ const symbolProfiles = await this.prismaService.symbolProfile.findMany({
orderBy: [{ symbol: 'asc' }] orderBy: [{ symbol: 'asc' }]
}); });
@ -305,7 +316,7 @@ export class DataGatheringService {
} }
private async getAssetProfileIdentifiersWithCompleteMarketData(): Promise< private async getAssetProfileIdentifiersWithCompleteMarketData(): Promise<
UniqueAsset[] AssetProfileIdentifier[]
> { > {
return ( return (
await this.prismaService.marketData.groupBy({ await this.prismaService.marketData.groupBy({

14
apps/api/src/services/data-provider/data-provider.service.ts

@ -20,7 +20,7 @@ import {
getStartOfUtcDate, getStartOfUtcDate,
isDerivedCurrency isDerivedCurrency
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import type { Granularity, UserWithSettings } from '@ghostfolio/common/types'; import type { Granularity, UserWithSettings } from '@ghostfolio/common/types';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
@ -75,7 +75,7 @@ export class DataProviderService {
return false; return false;
} }
public async getAssetProfiles(items: UniqueAsset[]): Promise<{ public async getAssetProfiles(items: AssetProfileIdentifier[]): Promise<{
[symbol: string]: Partial<SymbolProfile>; [symbol: string]: Partial<SymbolProfile>;
}> { }> {
const response: { const response: {
@ -173,7 +173,7 @@ export class DataProviderService {
} }
public async getHistorical( public async getHistorical(
aItems: UniqueAsset[], aItems: AssetProfileIdentifier[],
aGranularity: Granularity = 'month', aGranularity: Granularity = 'month',
from: Date, from: Date,
to: Date to: Date
@ -243,7 +243,7 @@ export class DataProviderService {
from, from,
to to
}: { }: {
dataGatheringItems: UniqueAsset[]; dataGatheringItems: AssetProfileIdentifier[];
from: Date; from: Date;
to: Date; to: Date;
}): Promise<{ }): Promise<{
@ -350,7 +350,7 @@ export class DataProviderService {
useCache = true, useCache = true,
user user
}: { }: {
items: UniqueAsset[]; items: AssetProfileIdentifier[];
requestTimeout?: number; requestTimeout?: number;
useCache?: boolean; useCache?: boolean;
user?: UserWithSettings; user?: UserWithSettings;
@ -376,7 +376,7 @@ export class DataProviderService {
} }
// Get items from cache // Get items from cache
const itemsToFetch: UniqueAsset[] = []; const itemsToFetch: AssetProfileIdentifier[] = [];
for (const { dataSource, symbol } of items) { for (const { dataSource, symbol } of items) {
if (useCache) { if (useCache) {
@ -633,7 +633,7 @@ export class DataProviderService {
dataGatheringItems dataGatheringItems
}: { }: {
currency: string; currency: string;
dataGatheringItems: UniqueAsset[]; dataGatheringItems: AssetProfileIdentifier[];
}) { }) {
return dataGatheringItems.some(({ dataSource, symbol }) => { return dataGatheringItems.some(({ dataSource, symbol }) => {
return ( return (

24
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts

@ -361,13 +361,13 @@ export class ExchangeRateDataService {
const symbol = `${currencyFrom}${currencyTo}`; const symbol = `${currencyFrom}${currencyTo}`;
const marketData = await this.marketDataService.getRange({ const marketData = await this.marketDataService.getRange({
dateQuery: { gte: startDate, lt: endDate }, assetProfileIdentifiers: [
uniqueAssets: [
{ {
dataSource, dataSource,
symbol symbol
} }
] ],
dateQuery: { gte: startDate, lt: endDate }
}); });
if (marketData?.length > 0) { if (marketData?.length > 0) {
@ -392,13 +392,13 @@ export class ExchangeRateDataService {
} }
} else { } else {
const marketData = await this.marketDataService.getRange({ const marketData = await this.marketDataService.getRange({
dateQuery: { gte: startDate, lt: endDate }, assetProfileIdentifiers: [
uniqueAssets: [
{ {
dataSource, dataSource,
symbol: `${DEFAULT_CURRENCY}${currencyFrom}` symbol: `${DEFAULT_CURRENCY}${currencyFrom}`
} }
] ],
dateQuery: { gte: startDate, lt: endDate }
}); });
for (const { date, marketPrice } of marketData) { for (const { date, marketPrice } of marketData) {
@ -415,16 +415,16 @@ export class ExchangeRateDataService {
} }
} else { } else {
const marketData = await this.marketDataService.getRange({ const marketData = await this.marketDataService.getRange({
dateQuery: { assetProfileIdentifiers: [
gte: startDate,
lt: endDate
},
uniqueAssets: [
{ {
dataSource, dataSource,
symbol: `${DEFAULT_CURRENCY}${currencyTo}` symbol: `${DEFAULT_CURRENCY}${currencyTo}`
} }
] ],
dateQuery: {
gte: startDate,
lt: endDate
}
}); });
for (const { date, marketPrice } of marketData) { for (const { date, marketPrice } of marketData) {

7
apps/api/src/services/interfaces/interfaces.ts

@ -1,4 +1,7 @@
import { DataProviderInfo, UniqueAsset } from '@ghostfolio/common/interfaces'; import {
AssetProfileIdentifier,
DataProviderInfo
} from '@ghostfolio/common/interfaces';
import { MarketState } from '@ghostfolio/common/types'; import { MarketState } from '@ghostfolio/common/types';
import { import {
@ -34,6 +37,6 @@ export interface IDataProviderResponse {
marketState: MarketState; marketState: MarketState;
} }
export interface IDataGatheringItem extends UniqueAsset { export interface IDataGatheringItem extends AssetProfileIdentifier {
date?: Date; date?: Date;
} }

16
apps/api/src/services/market-data/market-data.service.ts

@ -3,7 +3,7 @@ import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.i
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { import {
@ -17,7 +17,7 @@ import {
export class MarketDataService { export class MarketDataService {
public constructor(private readonly prismaService: PrismaService) {} public constructor(private readonly prismaService: PrismaService) {}
public async deleteMany({ dataSource, symbol }: UniqueAsset) { public async deleteMany({ dataSource, symbol }: AssetProfileIdentifier) {
return this.prismaService.marketData.deleteMany({ return this.prismaService.marketData.deleteMany({
where: { where: {
dataSource, dataSource,
@ -40,7 +40,7 @@ export class MarketDataService {
}); });
} }
public async getMax({ dataSource, symbol }: UniqueAsset) { public async getMax({ dataSource, symbol }: AssetProfileIdentifier) {
return this.prismaService.marketData.findFirst({ return this.prismaService.marketData.findFirst({
select: { select: {
date: true, date: true,
@ -59,11 +59,11 @@ export class MarketDataService {
} }
public async getRange({ public async getRange({
dateQuery, assetProfileIdentifiers,
uniqueAssets dateQuery
}: { }: {
assetProfileIdentifiers: AssetProfileIdentifier[];
dateQuery: DateQuery; dateQuery: DateQuery;
uniqueAssets: UniqueAsset[];
}): Promise<MarketData[]> { }): Promise<MarketData[]> {
return this.prismaService.marketData.findMany({ return this.prismaService.marketData.findMany({
orderBy: [ orderBy: [
@ -76,13 +76,13 @@ export class MarketDataService {
], ],
where: { where: {
dataSource: { dataSource: {
in: uniqueAssets.map(({ dataSource }) => { in: assetProfileIdentifiers.map(({ dataSource }) => {
return dataSource; return dataSource;
}) })
}, },
date: dateQuery, date: dateQuery,
symbol: { symbol: {
in: uniqueAssets.map(({ symbol }) => { in: assetProfileIdentifiers.map(({ symbol }) => {
return symbol; return symbol;
}) })
} }

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

@ -1,10 +1,10 @@
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 { import {
AssetProfileIdentifier,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Holding, Holding,
ScraperConfiguration, ScraperConfiguration
UniqueAsset
} from '@ghostfolio/common/interfaces'; } 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';
@ -23,7 +23,7 @@ export class SymbolProfileService {
return this.prismaService.symbolProfile.create({ data: assetProfile }); return this.prismaService.symbolProfile.create({ data: assetProfile });
} }
public async delete({ dataSource, symbol }: UniqueAsset) { public async delete({ dataSource, symbol }: AssetProfileIdentifier) {
return this.prismaService.symbolProfile.delete({ return this.prismaService.symbolProfile.delete({
where: { dataSource_symbol: { dataSource, symbol } } where: { dataSource_symbol: { dataSource, symbol } }
}); });
@ -36,7 +36,7 @@ export class SymbolProfileService {
} }
public async getSymbolProfiles( public async getSymbolProfiles(
aUniqueAssets: UniqueAsset[] aAssetProfileIdentifiers: AssetProfileIdentifier[]
): Promise<EnhancedSymbolProfile[]> { ): Promise<EnhancedSymbolProfile[]> {
return this.prismaService.symbolProfile return this.prismaService.symbolProfile
.findMany({ .findMany({
@ -54,7 +54,7 @@ export class SymbolProfileService {
SymbolProfileOverrides: true SymbolProfileOverrides: true
}, },
where: { where: {
OR: aUniqueAssets.map(({ dataSource, symbol }) => { OR: aAssetProfileIdentifiers.map(({ dataSource, symbol }) => {
return { return {
dataSource, dataSource,
symbol symbol
@ -140,7 +140,7 @@ export class SymbolProfileService {
symbolMapping, symbolMapping,
SymbolProfileOverrides, SymbolProfileOverrides,
url url
}: Prisma.SymbolProfileUpdateInput & UniqueAsset) { }: AssetProfileIdentifier & Prisma.SymbolProfileUpdateInput) {
return this.prismaService.symbolProfile.update({ return this.prismaService.symbolProfile.update({
data: { data: {
assetClass, assetClass,

16
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -7,9 +7,9 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier,
Filter, Filter,
InfoItem, InfoItem,
UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
@ -225,7 +225,7 @@ export class AdminMarketDataComponent
}); });
} }
public onDeleteAssetProfile({ dataSource, symbol }: UniqueAsset) { public onDeleteAssetProfile({ dataSource, symbol }: AssetProfileIdentifier) {
this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol }); this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol });
} }
@ -266,21 +266,27 @@ export class AdminMarketDataComponent
.subscribe(() => {}); .subscribe(() => {});
} }
public onGatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) { public onGatherProfileDataBySymbol({
dataSource,
symbol
}: AssetProfileIdentifier) {
this.adminService this.adminService
.gatherProfileDataBySymbol({ dataSource, symbol }) .gatherProfileDataBySymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {}); .subscribe(() => {});
} }
public onGatherSymbol({ dataSource, symbol }: UniqueAsset) { public onGatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) {
this.adminService this.adminService
.gatherSymbol({ dataSource, symbol }) .gatherSymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {}); .subscribe(() => {});
} }
public onOpenAssetProfileDialog({ dataSource, symbol }: UniqueAsset) { public onOpenAssetProfileDialog({
dataSource,
symbol
}: AssetProfileIdentifier) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { queryParams: {
dataSource, dataSource,

18
apps/client/src/app/components/admin-market-data/admin-market-data.service.ts

@ -2,8 +2,8 @@ import { AdminService } from '@ghostfolio/client/services/admin.service';
import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config'; import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config';
import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper'; import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
import { import {
AdminMarketDataItem, AssetProfileIdentifier,
UniqueAsset AdminMarketDataItem
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
@ -13,7 +13,7 @@ import { EMPTY, catchError, finalize, forkJoin, takeUntil } from 'rxjs';
export class AdminMarketDataService { export class AdminMarketDataService {
public constructor(private adminService: AdminService) {} public constructor(private adminService: AdminService) {}
public deleteAssetProfile({ dataSource, symbol }: UniqueAsset) { public deleteAssetProfile({ dataSource, symbol }: AssetProfileIdentifier) {
const confirmation = confirm( const confirmation = confirm(
$localize`Do you really want to delete this asset profile?` $localize`Do you really want to delete this asset profile?`
); );
@ -29,15 +29,19 @@ export class AdminMarketDataService {
} }
} }
public deleteAssetProfiles(uniqueAssets: UniqueAsset[]) { public deleteAssetProfiles(
aAssetProfileIdentifiers: AssetProfileIdentifier[]
) {
const confirmation = confirm( const confirmation = confirm(
$localize`Do you really want to delete these profiles?` $localize`Do you really want to delete these profiles?`
); );
if (confirmation) { if (confirmation) {
const deleteRequests = uniqueAssets.map(({ dataSource, symbol }) => { const deleteRequests = aAssetProfileIdentifiers.map(
return this.adminService.deleteProfileData({ dataSource, symbol }); ({ dataSource, symbol }) => {
}); return this.adminService.deleteProfileData({ dataSource, symbol });
}
);
forkJoin(deleteRequests) forkJoin(deleteRequests)
.pipe( .pipe(

15
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -8,7 +8,7 @@ import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
AdminMarketDataDetails, AdminMarketDataDetails,
UniqueAsset AssetProfileIdentifier
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
@ -175,20 +175,23 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
this.dialogRef.close(); this.dialogRef.close();
} }
public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) { public onDeleteProfileData({ dataSource, symbol }: AssetProfileIdentifier) {
this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol }); this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol });
this.dialogRef.close(); this.dialogRef.close();
} }
public onGatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) { public onGatherProfileDataBySymbol({
dataSource,
symbol
}: AssetProfileIdentifier) {
this.adminService this.adminService
.gatherProfileDataBySymbol({ dataSource, symbol }) .gatherProfileDataBySymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {}); .subscribe(() => {});
} }
public onGatherSymbol({ dataSource, symbol }: UniqueAsset) { public onGatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) {
this.adminService this.adminService
.gatherSymbol({ dataSource, symbol }) .gatherSymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
@ -242,7 +245,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
} }
} }
public onSetBenchmark({ dataSource, symbol }: UniqueAsset) { public onSetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
this.dataService this.dataService
.postBenchmark({ dataSource, symbol }) .postBenchmark({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
@ -342,7 +345,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}); });
} }
public onUnsetBenchmark({ dataSource, symbol }: UniqueAsset) { public onUnsetBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
this.dataService this.dataService
.deleteBenchmark({ dataSource, symbol }) .deleteBenchmark({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

4
apps/client/src/app/components/home-holdings/home-holdings.component.ts

@ -2,8 +2,8 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
AssetProfileIdentifier,
PortfolioPosition, PortfolioPosition,
UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -108,7 +108,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
this.initialize(); this.initialize();
} }
public onSymbolClicked({ dataSource, symbol }: UniqueAsset) { public onSymbolClicked({ dataSource, symbol }: AssetProfileIdentifier) {
if (dataSource && symbol) { if (dataSource && symbol) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { dataSource, symbol, holdingDetailDialog: true } queryParams: { dataSource, symbol, holdingDetailDialog: true }

4
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -5,9 +5,9 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { NUMERICAL_PRECISION_THRESHOLD } from '@ghostfolio/common/config'; import { NUMERICAL_PRECISION_THRESHOLD } from '@ghostfolio/common/config';
import { import {
AssetProfileIdentifier,
LineChartItem, LineChartItem,
PortfolioPerformance, PortfolioPerformance,
UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -26,7 +26,7 @@ import { takeUntil } from 'rxjs/operators';
export class HomeOverviewComponent implements OnDestroy, OnInit { export class HomeOverviewComponent implements OnDestroy, OnInit {
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS; public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public deviceType: string; public deviceType: string;
public errors: UniqueAsset[]; public errors: AssetProfileIdentifier[];
public hasError: boolean; public hasError: boolean;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean; public hasPermissionToCreateOrder: boolean;

19
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -38,6 +38,7 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces';
export class ImportActivitiesDialog implements OnDestroy { export class ImportActivitiesDialog implements OnDestroy {
public accounts: CreateAccountDto[] = []; public accounts: CreateAccountDto[] = [];
public activities: Activity[] = []; public activities: Activity[] = [];
public assetProfileForm: FormGroup;
public dataSource: MatTableDataSource<Activity>; public dataSource: MatTableDataSource<Activity>;
public details: any[] = []; public details: any[] = [];
public deviceType: string; public deviceType: string;
@ -53,7 +54,6 @@ export class ImportActivitiesDialog implements OnDestroy {
public sortDirection: SortDirection = 'desc'; public sortDirection: SortDirection = 'desc';
public stepperOrientation: StepperOrientation; public stepperOrientation: StepperOrientation;
public totalItems: number; public totalItems: number;
public uniqueAssetForm: FormGroup;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -73,8 +73,8 @@ export class ImportActivitiesDialog implements OnDestroy {
this.stepperOrientation = this.stepperOrientation =
this.deviceType === 'mobile' ? 'vertical' : 'horizontal'; this.deviceType === 'mobile' ? 'vertical' : 'horizontal';
this.uniqueAssetForm = this.formBuilder.group({ this.assetProfileForm = this.formBuilder.group({
uniqueAsset: [undefined, Validators.required] assetProfileIdentifier: [undefined, Validators.required]
}); });
if ( if (
@ -85,7 +85,7 @@ export class ImportActivitiesDialog implements OnDestroy {
this.dialogTitle = $localize`Import Dividends`; this.dialogTitle = $localize`Import Dividends`;
this.mode = 'DIVIDEND'; this.mode = 'DIVIDEND';
this.uniqueAssetForm.get('uniqueAsset').disable(); this.assetProfileForm.get('assetProfileIdentifier').disable();
this.dataService this.dataService
.fetchPortfolioHoldings({ .fetchPortfolioHoldings({
@ -102,7 +102,7 @@ export class ImportActivitiesDialog implements OnDestroy {
this.holdings = sortBy(holdings, ({ name }) => { this.holdings = sortBy(holdings, ({ name }) => {
return name.toLowerCase(); return name.toLowerCase();
}); });
this.uniqueAssetForm.get('uniqueAsset').enable(); this.assetProfileForm.get('assetProfileIdentifier').enable();
this.isLoading = false; this.isLoading = false;
@ -167,10 +167,11 @@ export class ImportActivitiesDialog implements OnDestroy {
} }
public onLoadDividends(aStepper: MatStepper) { public onLoadDividends(aStepper: MatStepper) {
this.uniqueAssetForm.get('uniqueAsset').disable(); this.assetProfileForm.get('assetProfileIdentifier').disable();
const { dataSource, symbol } = const { dataSource, symbol } = this.assetProfileForm.get(
this.uniqueAssetForm.get('uniqueAsset').value; 'assetProfileIdentifier'
).value;
this.dataService this.dataService
.fetchDividendsImport({ .fetchDividendsImport({
@ -193,7 +194,7 @@ export class ImportActivitiesDialog implements OnDestroy {
this.details = []; this.details = [];
this.errorMessages = []; this.errorMessages = [];
this.importStep = ImportStep.SELECT_ACTIVITIES; this.importStep = ImportStep.SELECT_ACTIVITIES;
this.uniqueAssetForm.get('uniqueAsset').enable(); this.assetProfileForm.get('assetProfileIdentifier').enable();
aStepper.reset(); aStepper.reset();
} }

8
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html

@ -25,14 +25,14 @@
<div class="pt-3"> <div class="pt-3">
@if (mode === 'DIVIDEND') { @if (mode === 'DIVIDEND') {
<form <form
[formGroup]="uniqueAssetForm" [formGroup]="assetProfileForm"
(ngSubmit)="onLoadDividends(stepper)" (ngSubmit)="onLoadDividends(stepper)"
> >
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Holding</mat-label> <mat-label i18n>Holding</mat-label>
<mat-select formControlName="uniqueAsset"> <mat-select formControlName="assetProfileIdentifier">
<mat-select-trigger>{{ <mat-select-trigger>{{
uniqueAssetForm.get('uniqueAsset')?.value?.name assetProfileForm.get('assetProfileIdentifier')?.value?.name
}}</mat-select-trigger> }}</mat-select-trigger>
@for (holding of holdings; track holding) { @for (holding of holdings; track holding) {
<mat-option <mat-option
@ -63,7 +63,7 @@
color="primary" color="primary"
mat-flat-button mat-flat-button
type="submit" type="submit"
[disabled]="!uniqueAssetForm.valid" [disabled]="!assetProfileForm.valid"
> >
<span i18n>Load Dividends</span> <span i18n>Load Dividends</span>
</button> </button>

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

@ -6,10 +6,10 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config'; import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config';
import { prettifySymbol } from '@ghostfolio/common/helper'; import { prettifySymbol } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier,
Holding, Holding,
PortfolioDetails, PortfolioDetails,
PortfolioPosition, PortfolioPosition,
UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Market, MarketAdvanced } from '@ghostfolio/common/types'; import { Market, MarketAdvanced } from '@ghostfolio/common/types';
@ -161,7 +161,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.initialize(); this.initialize();
} }
public onAccountChartClicked({ symbol }: UniqueAsset) { public onAccountChartClicked({ symbol }: AssetProfileIdentifier) {
if (symbol && symbol !== UNKNOWN_KEY) { if (symbol && symbol !== UNKNOWN_KEY) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { accountId: symbol, accountDetailDialog: true } queryParams: { accountId: symbol, accountDetailDialog: true }
@ -169,7 +169,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
} }
} }
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) { public onSymbolChartClicked({ dataSource, symbol }: AssetProfileIdentifier) {
if (dataSource && symbol) { if (dataSource && symbol) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { dataSource, symbol, holdingDetailDialog: true } queryParams: { dataSource, symbol, holdingDetailDialog: true }

19
apps/client/src/app/services/admin.service.ts

@ -7,13 +7,13 @@ import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier,
AdminData, AdminData,
AdminJobs, AdminJobs,
AdminMarketData, AdminMarketData,
AdminMarketDataDetails, AdminMarketDataDetails,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Filter, Filter
UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
@ -35,7 +35,7 @@ export class AdminService {
private http: HttpClient private http: HttpClient
) {} ) {}
public addAssetProfile({ dataSource, symbol }: UniqueAsset) { public addAssetProfile({ dataSource, symbol }: AssetProfileIdentifier) {
return this.http.post<void>( return this.http.post<void>(
`/api/v1/admin/profile-data/${dataSource}/${symbol}`, `/api/v1/admin/profile-data/${dataSource}/${symbol}`,
null null
@ -62,7 +62,7 @@ export class AdminService {
return this.http.delete<void>(`/api/v1/platform/${aId}`); return this.http.delete<void>(`/api/v1/platform/${aId}`);
} }
public deleteProfileData({ dataSource, symbol }: UniqueAsset) { public deleteProfileData({ dataSource, symbol }: AssetProfileIdentifier) {
return this.http.delete<void>( return this.http.delete<void>(
`/api/v1/admin/profile-data/${dataSource}/${symbol}` `/api/v1/admin/profile-data/${dataSource}/${symbol}`
); );
@ -167,7 +167,10 @@ export class AdminService {
return this.http.post<void>('/api/v1/admin/gather/profile-data', {}); return this.http.post<void>('/api/v1/admin/gather/profile-data', {});
} }
public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) { public gatherProfileDataBySymbol({
dataSource,
symbol
}: AssetProfileIdentifier) {
return this.http.post<void>( return this.http.post<void>(
`/api/v1/admin/gather/profile-data/${dataSource}/${symbol}`, `/api/v1/admin/gather/profile-data/${dataSource}/${symbol}`,
{} {}
@ -178,7 +181,7 @@ export class AdminService {
dataSource, dataSource,
date, date,
symbol symbol
}: UniqueAsset & { }: AssetProfileIdentifier & {
date?: Date; date?: Date;
}) { }) {
let url = `/api/v1/admin/gather/${dataSource}/${symbol}`; let url = `/api/v1/admin/gather/${dataSource}/${symbol}`;
@ -217,7 +220,7 @@ export class AdminService {
symbol, symbol,
symbolMapping, symbolMapping,
url url
}: UniqueAsset & UpdateAssetProfileDto) { }: AssetProfileIdentifier & UpdateAssetProfileDto) {
return this.http.patch<EnhancedSymbolProfile>( return this.http.patch<EnhancedSymbolProfile>(
`/api/v1/admin/profile-data/${dataSource}/${symbol}`, `/api/v1/admin/profile-data/${dataSource}/${symbol}`,
{ {
@ -272,7 +275,7 @@ export class AdminService {
dataSource, dataSource,
scraperConfiguration, scraperConfiguration,
symbol symbol
}: UniqueAsset & UpdateAssetProfileDto['scraperConfiguration']) { }: AssetProfileIdentifier & UpdateAssetProfileDto['scraperConfiguration']) {
return this.http.post<any>( return this.http.post<any>(
`/api/v1/admin/market-data/${dataSource}/${symbol}/test`, `/api/v1/admin/market-data/${dataSource}/${symbol}/test`,
{ {

14
apps/client/src/app/services/data.service.ts

@ -20,6 +20,7 @@ import {
AccountBalancesResponse, AccountBalancesResponse,
Accounts, Accounts,
AdminMarketDataDetails, AdminMarketDataDetails,
AssetProfileIdentifier,
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,
BenchmarkResponse, BenchmarkResponse,
Export, Export,
@ -34,7 +35,6 @@ import {
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPublicDetails, PortfolioPublicDetails,
PortfolioReport, PortfolioReport,
UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions'; import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
@ -230,7 +230,7 @@ export class DataService {
}); });
} }
public fetchDividendsImport({ dataSource, symbol }: UniqueAsset) { public fetchDividendsImport({ dataSource, symbol }: AssetProfileIdentifier) {
return this.http.get<ImportResponse>( return this.http.get<ImportResponse>(
`/api/v1/import/dividends/${dataSource}/${symbol}` `/api/v1/import/dividends/${dataSource}/${symbol}`
); );
@ -270,7 +270,7 @@ export class DataService {
return this.http.delete<any>(`/api/v1/order/${aId}`); return this.http.delete<any>(`/api/v1/order/${aId}`);
} }
public deleteBenchmark({ dataSource, symbol }: UniqueAsset) { public deleteBenchmark({ dataSource, symbol }: AssetProfileIdentifier) {
return this.http.delete<any>(`/api/v1/benchmark/${dataSource}/${symbol}`); return this.http.delete<any>(`/api/v1/benchmark/${dataSource}/${symbol}`);
} }
@ -289,7 +289,7 @@ export class DataService {
public fetchAsset({ public fetchAsset({
dataSource, dataSource,
symbol symbol
}: UniqueAsset): Observable<AdminMarketDataDetails> { }: AssetProfileIdentifier): Observable<AdminMarketDataDetails> {
return this.http.get<any>(`/api/v1/asset/${dataSource}/${symbol}`).pipe( return this.http.get<any>(`/api/v1/asset/${dataSource}/${symbol}`).pipe(
map((data) => { map((data) => {
for (const item of data.marketData) { for (const item of data.marketData) {
@ -308,7 +308,7 @@ export class DataService {
}: { }: {
range: DateRange; range: DateRange;
startDate: Date; startDate: Date;
} & UniqueAsset): Observable<BenchmarkMarketDataDetails> { } & AssetProfileIdentifier): Observable<BenchmarkMarketDataDetails> {
let params = new HttpParams(); let params = new HttpParams();
if (range) { if (range) {
@ -630,7 +630,7 @@ export class DataService {
); );
} }
public postBenchmark(benchmark: UniqueAsset) { public postBenchmark(benchmark: AssetProfileIdentifier) {
return this.http.post(`/api/v1/benchmark`, benchmark); return this.http.post(`/api/v1/benchmark`, benchmark);
} }
@ -654,7 +654,7 @@ export class DataService {
dataSource, dataSource,
symbol, symbol,
tags tags
}: { tags: Tag[] } & UniqueAsset) { }: { tags: Tag[] } & AssetProfileIdentifier) {
return this.http.put<void>( return this.http.put<void>(
`/api/v1/portfolio/position/${dataSource}/${symbol}/tags`, `/api/v1/portfolio/position/${dataSource}/${symbol}/tags`,
{ tags } { tags }

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

@ -19,7 +19,7 @@ import {
ghostfolioScraperApiSymbolPrefix, ghostfolioScraperApiSymbolPrefix,
locale locale
} from './config'; } from './config';
import { Benchmark, UniqueAsset } from './interfaces'; import { AssetProfileIdentifier, Benchmark } from './interfaces';
import { BenchmarkTrend, ColorScheme } from './types'; import { BenchmarkTrend, ColorScheme } from './types';
export const DATE_FORMAT = 'yyyy-MM-dd'; export const DATE_FORMAT = 'yyyy-MM-dd';
@ -147,7 +147,10 @@ export function getAllActivityTypes(): ActivityType[] {
return Object.values(ActivityType); return Object.values(ActivityType);
} }
export function getAssetProfileIdentifier({ dataSource, symbol }: UniqueAsset) { export function getAssetProfileIdentifier({
dataSource,
symbol
}: AssetProfileIdentifier) {
return `${dataSource}-${symbol}`; return `${dataSource}-${symbol}`;
} }
@ -377,7 +380,7 @@ export function parseDate(date: string): Date | null {
return parseISO(date); return parseISO(date);
} }
export function parseSymbol({ dataSource, symbol }: UniqueAsset) { export function parseSymbol({ dataSource, symbol }: AssetProfileIdentifier) {
const [ticker, exchange] = symbol.split('.'); const [ticker, exchange] = symbol.split('.');
return { return {

6
libs/common/src/lib/interfaces/admin-data.interface.ts

@ -1,13 +1,13 @@
import { Role } from '@prisma/client'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { UniqueAsset } from './unique-asset.interface'; import { Role } from '@prisma/client';
export interface AdminData { export interface AdminData {
exchangeRates: ({ exchangeRates: ({
label1: string; label1: string;
label2: string; label2: string;
value: number; value: number;
} & UniqueAsset)[]; } & AssetProfileIdentifier)[];
settings: { [key: string]: boolean | object | string | string[] }; settings: { [key: string]: boolean | object | string | string[] };
transactionCount: number; transactionCount: number;
userCount: number; userCount: number;

2
libs/common/src/lib/interfaces/unique-asset.interface.ts → libs/common/src/lib/interfaces/asset-profile-identifier.interface.ts

@ -1,6 +1,6 @@
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
export interface UniqueAsset { export interface AssetProfileIdentifier {
dataSource: DataSource; dataSource: DataSource;
symbol: string; symbol: string;
} }

4
libs/common/src/lib/interfaces/index.ts

@ -7,6 +7,7 @@ import type {
AdminMarketData, AdminMarketData,
AdminMarketDataItem AdminMarketDataItem
} from './admin-market-data.interface'; } from './admin-market-data.interface';
import type { AssetProfileIdentifier } from './asset-profile-identifier.interface';
import type { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface'; import type { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
import type { BenchmarkProperty } from './benchmark-property.interface'; import type { BenchmarkProperty } from './benchmark-property.interface';
import type { Benchmark } from './benchmark.interface'; import type { Benchmark } from './benchmark.interface';
@ -48,7 +49,6 @@ import type { Subscription } from './subscription.interface';
import type { SymbolMetrics } from './symbol-metrics.interface'; import type { SymbolMetrics } from './symbol-metrics.interface';
import type { SystemMessage } from './system-message.interface'; import type { SystemMessage } from './system-message.interface';
import type { TabConfiguration } from './tab-configuration.interface'; import type { TabConfiguration } from './tab-configuration.interface';
import type { UniqueAsset } from './unique-asset.interface';
import type { UserSettings } from './user-settings.interface'; import type { UserSettings } from './user-settings.interface';
import type { User } from './user.interface'; import type { User } from './user.interface';
@ -61,6 +61,7 @@ export {
AdminMarketData, AdminMarketData,
AdminMarketDataDetails, AdminMarketDataDetails,
AdminMarketDataItem, AdminMarketDataItem,
AssetProfileIdentifier,
Benchmark, Benchmark,
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,
BenchmarkProperty, BenchmarkProperty,
@ -101,7 +102,6 @@ export {
Subscription, Subscription,
SymbolMetrics, SymbolMetrics,
TabConfiguration, TabConfiguration,
UniqueAsset,
User, User,
UserSettings UserSettings
}; };

4
libs/common/src/lib/interfaces/responses/errors.interface.ts

@ -1,6 +1,6 @@
import { UniqueAsset } from '../unique-asset.interface'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
export interface ResponseError { export interface ResponseError {
errors?: UniqueAsset[]; errors?: AssetProfileIdentifier[];
hasErrors: boolean; hasErrors: boolean;
} }

4
libs/common/src/lib/models/portfolio-snapshot.ts

@ -1,5 +1,5 @@
import { transformToBig } from '@ghostfolio/common/class-transformer'; import { transformToBig } from '@ghostfolio/common/class-transformer';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models'; import { TimelinePosition } from '@ghostfolio/common/models';
import { Big } from 'big.js'; import { Big } from 'big.js';
@ -9,7 +9,7 @@ export class PortfolioSnapshot {
@Transform(transformToBig, { toClassOnly: true }) @Transform(transformToBig, { toClassOnly: true })
@Type(() => Big) @Type(() => Big)
currentValueInBaseCurrency: Big; currentValueInBaseCurrency: Big;
errors?: UniqueAsset[]; errors?: AssetProfileIdentifier[];
@Transform(transformToBig, { toClassOnly: true }) @Transform(transformToBig, { toClassOnly: true })
@Type(() => Big) @Type(() => Big)

6
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -3,7 +3,7 @@ import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { getDateFormatString, getLocale } from '@ghostfolio/common/helper'; import { getDateFormatString, getLocale } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { GfActivityTypeComponent } from '@ghostfolio/ui/activity-type'; import { GfActivityTypeComponent } from '@ghostfolio/ui/activity-type';
import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info'; import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info';
@ -99,7 +99,7 @@ export class GfActivitiesTableComponent
@Output() export = new EventEmitter<void>(); @Output() export = new EventEmitter<void>();
@Output() exportDrafts = new EventEmitter<string[]>(); @Output() exportDrafts = new EventEmitter<string[]>();
@Output() import = new EventEmitter<void>(); @Output() import = new EventEmitter<void>();
@Output() importDividends = new EventEmitter<UniqueAsset>(); @Output() importDividends = new EventEmitter<AssetProfileIdentifier>();
@Output() pageChanged = new EventEmitter<PageEvent>(); @Output() pageChanged = new EventEmitter<PageEvent>();
@Output() selectedActivities = new EventEmitter<Activity[]>(); @Output() selectedActivities = new EventEmitter<Activity[]>();
@Output() sortChanged = new EventEmitter<Sort>(); @Output() sortChanged = new EventEmitter<Sort>();
@ -263,7 +263,7 @@ export class GfActivitiesTableComponent
alert(aComment); alert(aComment);
} }
public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset) { public onOpenPositionDialog({ dataSource, symbol }: AssetProfileIdentifier) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { dataSource, symbol, holdingDetailDialog: true } queryParams: { dataSource, symbol, holdingDetailDialog: true }
}); });

4
libs/ui/src/lib/assistant/interfaces/interfaces.ts

@ -1,4 +1,4 @@
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
export interface IDateRangeOption { export interface IDateRangeOption {
@ -6,7 +6,7 @@ export interface IDateRangeOption {
value: DateRange; value: DateRange;
} }
export interface ISearchResultItem extends UniqueAsset { export interface ISearchResultItem extends AssetProfileIdentifier {
assetSubClassString: string; assetSubClassString: string;
currency: string; currency: string;
name: string; name: string;

13
libs/ui/src/lib/benchmark/benchmark.component.ts

@ -1,5 +1,9 @@
import { getLocale, resolveMarketCondition } from '@ghostfolio/common/helper'; import { getLocale, resolveMarketCondition } from '@ghostfolio/common/helper';
import { Benchmark, UniqueAsset, User } from '@ghostfolio/common/interfaces'; import {
AssetProfileIdentifier,
Benchmark,
User
} from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { GfTrendIndicatorComponent } from '@ghostfolio/ui/trend-indicator'; import { GfTrendIndicatorComponent } from '@ghostfolio/ui/trend-indicator';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
@ -84,7 +88,7 @@ export class GfBenchmarkComponent implements OnChanges, OnDestroy {
} }
} }
public onOpenBenchmarkDialog({ dataSource, symbol }: UniqueAsset) { public onOpenBenchmarkDialog({ dataSource, symbol }: AssetProfileIdentifier) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { dataSource, symbol, benchmarkDetailDialog: true } queryParams: { dataSource, symbol, benchmarkDetailDialog: true }
}); });
@ -95,7 +99,10 @@ export class GfBenchmarkComponent implements OnChanges, OnDestroy {
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private openBenchmarkDetailDialog({ dataSource, symbol }: UniqueAsset) { private openBenchmarkDetailDialog({
dataSource,
symbol
}: AssetProfileIdentifier) {
const dialogRef = this.dialog.open(GfBenchmarkDetailDialogComponent, { const dialogRef = this.dialog.open(GfBenchmarkDetailDialogComponent, {
data: <BenchmarkDetailDialogParams>{ data: <BenchmarkDetailDialogParams>{
dataSource, dataSource,

7
libs/ui/src/lib/holdings-table/holdings-table.component.ts

@ -2,7 +2,10 @@ import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset
import { GfHoldingDetailDialogComponent } from '@ghostfolio/client/components/holding-detail-dialog/holding-detail-dialog.component'; import { GfHoldingDetailDialogComponent } from '@ghostfolio/client/components/holding-detail-dialog/holding-detail-dialog.component';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { getLocale } from '@ghostfolio/common/helper'; import { getLocale } from '@ghostfolio/common/helper';
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; import {
AssetProfileIdentifier,
PortfolioPosition
} from '@ghostfolio/common/interfaces';
import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info'; import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
@ -102,7 +105,7 @@ export class GfHoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
} }
} }
public onOpenHoldingDialog({ dataSource, symbol }: UniqueAsset) { public onOpenHoldingDialog({ dataSource, symbol }: AssetProfileIdentifier) {
if (this.hasPermissionToOpenDetails) { if (this.hasPermissionToOpenDetails) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { dataSource, symbol, holdingDetailDialog: true } queryParams: { dataSource, symbol, holdingDetailDialog: true }

7
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -1,7 +1,10 @@
import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; import { getTooltipOptions } from '@ghostfolio/common/chart-helper';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getLocale, getTextColor } from '@ghostfolio/common/helper'; import { getLocale, getTextColor } from '@ghostfolio/common/helper';
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; import {
AssetProfileIdentifier,
PortfolioPosition
} from '@ghostfolio/common/interfaces';
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
@ -71,7 +74,7 @@ export class GfPortfolioProportionChartComponent
}; };
} = {}; } = {};
@Output() proportionChartClicked = new EventEmitter<UniqueAsset>(); @Output() proportionChartClicked = new EventEmitter<AssetProfileIdentifier>();
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;

7
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -1,5 +1,8 @@
import { getAnnualizedPerformancePercent } from '@ghostfolio/common/calculation-helper'; import { getAnnualizedPerformancePercent } from '@ghostfolio/common/calculation-helper';
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; import {
AssetProfileIdentifier,
PortfolioPosition
} from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
@ -40,7 +43,7 @@ export class GfTreemapChartComponent
@Input() cursor: string; @Input() cursor: string;
@Input() holdings: PortfolioPosition[]; @Input() holdings: PortfolioPosition[];
@Output() treemapChartClicked = new EventEmitter<UniqueAsset>(); @Output() treemapChartClicked = new EventEmitter<AssetProfileIdentifier>();
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;

Loading…
Cancel
Save