Browse Source

Merge branch 'ghostfolio:main' into polish-translations

pull/3643/head
Paulina 1 year ago
committed by GitHub
parent
commit
a7d8ebd93d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      CHANGELOG.md
  2. 21
      apps/api/src/app/admin/admin.service.ts
  3. 8
      apps/api/src/app/benchmark/benchmark.controller.ts
  4. 15
      apps/api/src/app/benchmark/benchmark.service.ts
  5. 4
      apps/api/src/app/import/import.service.ts
  6. 4
      apps/api/src/app/logo/logo.service.ts
  7. 15
      apps/api/src/app/order/order.service.ts
  8. 7
      apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
  9. 22
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  10. 7
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
  11. 16
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  12. 15
      apps/api/src/app/portfolio/current-rate.service.ts
  13. 4
      apps/api/src/app/portfolio/interfaces/get-value-object.interface.ts
  14. 4
      apps/api/src/app/redis-cache/redis-cache.service.ts
  15. 7
      apps/api/src/app/symbol/interfaces/symbol-item.interface.ts
  16. 6
      apps/api/src/app/symbol/symbol.service.ts
  17. 4
      apps/api/src/services/data-gathering/data-gathering.processor.ts
  18. 41
      apps/api/src/services/data-gathering/data-gathering.service.ts
  19. 14
      apps/api/src/services/data-provider/data-provider.service.ts
  20. 24
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  21. 7
      apps/api/src/services/interfaces/interfaces.ts
  22. 16
      apps/api/src/services/market-data/market-data.service.ts
  23. 12
      apps/api/src/services/symbol-profile/symbol-profile.service.ts
  24. 16
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  25. 16
      apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
  26. 15
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  27. 4
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  28. 4
      apps/client/src/app/components/home-overview/home-overview.component.ts
  29. 9
      apps/client/src/app/pages/portfolio/activities/activities-page.component.ts
  30. 19
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  31. 8
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html
  32. 6
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  33. 19
      apps/client/src/app/services/admin.service.ts
  34. 14
      apps/client/src/app/services/data.service.ts
  35. 512
      apps/client/src/locales/messages.ca.xlf
  36. 486
      apps/client/src/locales/messages.de.xlf
  37. 486
      apps/client/src/locales/messages.es.xlf
  38. 486
      apps/client/src/locales/messages.fr.xlf
  39. 540
      apps/client/src/locales/messages.it.xlf
  40. 500
      apps/client/src/locales/messages.nl.xlf
  41. 486
      apps/client/src/locales/messages.pl.xlf
  42. 486
      apps/client/src/locales/messages.pt.xlf
  43. 524
      apps/client/src/locales/messages.tr.xlf
  44. 454
      apps/client/src/locales/messages.xlf
  45. 486
      apps/client/src/locales/messages.zh.xlf
  46. 9
      libs/common/src/lib/helper.ts
  47. 6
      libs/common/src/lib/interfaces/admin-data.interface.ts
  48. 2
      libs/common/src/lib/interfaces/asset-profile-identifier.interface.ts
  49. 4
      libs/common/src/lib/interfaces/index.ts
  50. 4
      libs/common/src/lib/interfaces/responses/errors.interface.ts
  51. 4
      libs/common/src/lib/models/portfolio-snapshot.ts
  52. 133
      libs/common/src/lib/personal-finance-tools.ts
  53. 6
      libs/ui/src/lib/activities-table/activities-table.component.ts
  54. 4
      libs/ui/src/lib/assistant/interfaces/interfaces.ts
  55. 13
      libs/ui/src/lib/benchmark/benchmark.component.ts
  56. 7
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  57. 7
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  58. 7
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
  59. 882
      package-lock.json
  60. 22
      package.json

8
CHANGELOG.md

@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Improved the language localization for German (`de`)
- Improved the language localization for Polish (`pl`)
- Upgraded `Nx` from version `19.5.1` to `19.5.6`
## 2.101.0 - 2024-08-03
### Changed

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

@ -21,9 +21,9 @@ import {
AdminMarketData,
AdminMarketDataDetails,
AdminMarketDataItem,
AssetProfileIdentifier,
EnhancedSymbolProfile,
Filter,
UniqueAsset
Filter
} from '@ghostfolio/common/interfaces';
import { MarketDataPreset } from '@ghostfolio/common/types';
@ -59,7 +59,9 @@ export class AdminService {
currency,
dataSource,
symbol
}: UniqueAsset & { currency?: string }): Promise<SymbolProfile | never> {
}: AssetProfileIdentifier & { currency?: string }): Promise<
SymbolProfile | never
> {
try {
if (dataSource === 'MANUAL') {
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.symbolProfileService.delete({ dataSource, symbol });
}
@ -325,7 +330,7 @@ export class AdminService {
public async getMarketDataBySymbol({
dataSource,
symbol
}: UniqueAsset): Promise<AdminMarketDataDetails> {
}: AssetProfileIdentifier): Promise<AdminMarketDataDetails> {
let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
let currency: EnhancedSymbolProfile['currency'] = '-';
let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
@ -386,7 +391,7 @@ export class AdminService {
symbol,
symbolMapping,
url
}: Prisma.SymbolProfileUpdateInput & UniqueAsset) {
}: AssetProfileIdentifier & Prisma.SymbolProfileUpdateInput) {
const symbolProfileOverrides = {
assetClass: assetClass as AssetClass,
assetSubClass: assetSubClass as AssetSubClass,
@ -394,8 +399,8 @@ export class AdminService {
url: url as string
};
const updatedSymbolProfile: Prisma.SymbolProfileUpdateInput & UniqueAsset =
{
const updatedSymbolProfile: AssetProfileIdentifier &
Prisma.SymbolProfileUpdateInput = {
comment,
countries,
currency,

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

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

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

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

@ -19,7 +19,7 @@ import {
getAssetProfileIdentifier,
parseDate
} from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import {
AccountWithPlatform,
OrderWithAccount,
@ -51,7 +51,7 @@ export class ImportService {
dataSource,
symbol,
userCurrency
}: UniqueAsset & { userCurrency: string }): Promise<Activity[]> {
}: AssetProfileIdentifier & { userCurrency: string }): Promise<Activity[]> {
try {
const { firstBuyDate, historicalData, orders } =
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 { 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 { DataSource } from '@prisma/client';
@ -17,7 +17,7 @@ export class LogoService {
public async getLogoByDataSourceAndSymbol({
dataSource,
symbol
}: UniqueAsset) {
}: AssetProfileIdentifier) {
if (!DataSource[dataSource]) {
throw new HttpException(
getReasonPhrase(StatusCodes.NOT_FOUND),

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

@ -11,9 +11,9 @@ import {
} from '@ghostfolio/common/config';
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
EnhancedSymbolProfile,
Filter,
UniqueAsset
Filter
} from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
@ -51,7 +51,7 @@ export class OrderService {
symbol,
tags,
userId
}: { tags: Tag[]; userId: string } & UniqueAsset) {
}: { tags: Tag[]; userId: string } & AssetProfileIdentifier) {
const orders = await this.prismaService.order.findMany({
where: {
userId,
@ -285,7 +285,7 @@ export class OrderService {
return count;
}
public async getLatestOrder({ dataSource, symbol }: UniqueAsset) {
public async getLatestOrder({ dataSource, symbol }: AssetProfileIdentifier) {
return this.prismaService.order.findFirst({
orderBy: {
date: 'desc'
@ -464,7 +464,7 @@ export class OrderService {
this.prismaService.order.count({ where })
]);
const uniqueAssets = uniqBy(
const assetProfileIdentifiers = uniqBy(
orders.map(({ SymbolProfile }) => {
return {
dataSource: SymbolProfile.dataSource,
@ -479,8 +479,9 @@ export class OrderService {
}
);
const assetProfiles =
await this.symbolProfileService.getSymbolProfiles(uniqueAssets);
const assetProfiles = await this.symbolProfileService.getSymbolProfiles(
assetProfileIdentifiers
);
const activities = orders.map((order) => {
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 { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces';
import {
AssetProfileIdentifier,
SymbolMetrics
} from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
export class MWRPortfolioCalculator extends PortfolioCalculator {
@ -27,7 +30,7 @@ export class MWRPortfolioCalculator extends PortfolioCalculator {
};
start: Date;
step?: number;
} & UniqueAsset): SymbolMetrics {
} & AssetProfileIdentifier): SymbolMetrics {
throw new Error('Method not implemented.');
}
}

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

@ -19,12 +19,12 @@ import {
resetHours
} from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
DataProviderInfo,
HistoricalDataItem,
InvestmentItem,
ResponseError,
SymbolMetrics,
UniqueAsset
SymbolMetrics
} from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models';
import { DateRange, GroupBy } from '@ghostfolio/common/types';
@ -356,15 +356,15 @@ export abstract class PortfolioCalculator {
dataSource: item.dataSource,
fee: item.fee,
firstBuyDate: item.firstBuyDate,
grossPerformance: !hasErrors ? grossPerformance ?? null : null,
grossPerformance: !hasErrors ? (grossPerformance ?? null) : null,
grossPerformancePercentage: !hasErrors
? grossPerformancePercentage ?? null
? (grossPerformancePercentage ?? null)
: null,
grossPerformancePercentageWithCurrencyEffect: !hasErrors
? grossPerformancePercentageWithCurrencyEffect ?? null
? (grossPerformancePercentageWithCurrencyEffect ?? null)
: null,
grossPerformanceWithCurrencyEffect: !hasErrors
? grossPerformanceWithCurrencyEffect ?? null
? (grossPerformanceWithCurrencyEffect ?? null)
: null,
investment: totalInvestment,
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
@ -372,15 +372,15 @@ export abstract class PortfolioCalculator {
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null,
marketPriceInBaseCurrency:
marketPriceInBaseCurrency?.toNumber() ?? null,
netPerformance: !hasErrors ? netPerformance ?? null : null,
netPerformance: !hasErrors ? (netPerformance ?? null) : null,
netPerformancePercentage: !hasErrors
? netPerformancePercentage ?? null
? (netPerformancePercentage ?? null)
: null,
netPerformancePercentageWithCurrencyEffect: !hasErrors
? netPerformancePercentageWithCurrencyEffect ?? null
? (netPerformancePercentageWithCurrencyEffect ?? null)
: null,
netPerformanceWithCurrencyEffect: !hasErrors
? netPerformanceWithCurrencyEffect ?? null
? (netPerformanceWithCurrencyEffect ?? null)
: null,
quantity: item.quantity,
symbol: item.symbol,
@ -905,7 +905,7 @@ export abstract class PortfolioCalculator {
};
start: Date;
step?: number;
} & UniqueAsset): SymbolMetrics;
} & AssetProfileIdentifier): SymbolMetrics;
public getTransactionPoints() {
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 { getFactor } from '@ghostfolio/api/helper/portfolio.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 { Logger } from '@nestjs/common';
@ -151,7 +154,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
};
start: Date;
step?: number;
} & UniqueAsset): SymbolMetrics {
} & AssetProfileIdentifier): SymbolMetrics {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const currentValues: { [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 { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.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';
@ -24,32 +24,32 @@ jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => {
});
},
getRange: ({
assetProfileIdentifiers,
dateRangeEnd,
dateRangeStart,
uniqueAssets
dateRangeStart
}: {
assetProfileIdentifiers: AssetProfileIdentifier[];
dateRangeEnd: Date;
dateRangeStart: Date;
uniqueAssets: UniqueAsset[];
}) => {
return Promise.resolve<MarketData[]>([
{
createdAt: dateRangeStart,
dataSource: uniqueAssets[0].dataSource,
dataSource: assetProfileIdentifiers[0].dataSource,
date: dateRangeStart,
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
marketPrice: 1841.823902,
state: 'CLOSE',
symbol: uniqueAssets[0].symbol
symbol: assetProfileIdentifiers[0].symbol
},
{
createdAt: dateRangeEnd,
dataSource: uniqueAssets[0].dataSource,
dataSource: assetProfileIdentifiers[0].dataSource,
date: dateRangeEnd,
id: '082d6893-df27-4c91-8a5d-092e84315b56',
marketPrice: 1847.839966,
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 { resetHours } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
DataProviderInfo,
ResponseError,
UniqueAsset
ResponseError
} from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -80,17 +80,16 @@ export class CurrentRateService {
);
}
const uniqueAssets: UniqueAsset[] = dataGatheringItems.map(
({ dataSource, symbol }) => {
const assetProfileIdentifiers: AssetProfileIdentifier[] =
dataGatheringItems.map(({ dataSource, symbol }) => {
return { dataSource, symbol };
}
);
});
promises.push(
this.marketDataService
.getRange({
dateQuery,
uniqueAssets
assetProfileIdentifiers,
dateQuery
})
.then((data) => {
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;
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 { 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 { Inject, Injectable, Logger } from '@nestjs/common';
@ -28,7 +28,7 @@ export class RedisCacheService {
return `portfolio-snapshot-${userId}`;
}
public getQuoteKey({ dataSource, symbol }: UniqueAsset) {
public getQuoteKey({ dataSource, symbol }: AssetProfileIdentifier) {
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;
historicalData: HistoricalDataItem[];
marketPrice: number;

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

@ -40,13 +40,13 @@ export class SymbolService {
const days = includeHistoricalData;
const marketData = await this.marketDataService.getRange({
dateQuery: { gte: subDays(new Date(), days) },
uniqueAssets: [
assetProfileIdentifiers: [
{
dataSource: dataGatheringItem.dataSource,
symbol: dataGatheringItem.symbol
}
]
],
dateQuery: { gte: subDays(new Date(), days) }
});
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
} from '@ghostfolio/common/config';
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 { Injectable, Logger } from '@nestjs/common';
@ -35,7 +35,7 @@ export class DataGatheringProcessor {
) {}
@Process({ concurrency: 1, name: GATHER_ASSET_PROFILE_PROCESS })
public async gatherAssetProfile(job: Job<UniqueAsset>) {
public async gatherAssetProfile(job: Job<AssetProfileIdentifier>) {
try {
Logger.log(
`Asset profile data gathering has been started for ${job.data.symbol} (${job.data.dataSource})`,

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

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

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

@ -20,7 +20,7 @@ import {
getStartOfUtcDate,
isDerivedCurrency
} 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 { Inject, Injectable, Logger } from '@nestjs/common';
@ -75,7 +75,7 @@ export class DataProviderService {
return false;
}
public async getAssetProfiles(items: UniqueAsset[]): Promise<{
public async getAssetProfiles(items: AssetProfileIdentifier[]): Promise<{
[symbol: string]: Partial<SymbolProfile>;
}> {
const response: {
@ -173,7 +173,7 @@ export class DataProviderService {
}
public async getHistorical(
aItems: UniqueAsset[],
aItems: AssetProfileIdentifier[],
aGranularity: Granularity = 'month',
from: Date,
to: Date
@ -243,7 +243,7 @@ export class DataProviderService {
from,
to
}: {
dataGatheringItems: UniqueAsset[];
dataGatheringItems: AssetProfileIdentifier[];
from: Date;
to: Date;
}): Promise<{
@ -350,7 +350,7 @@ export class DataProviderService {
useCache = true,
user
}: {
items: UniqueAsset[];
items: AssetProfileIdentifier[];
requestTimeout?: number;
useCache?: boolean;
user?: UserWithSettings;
@ -376,7 +376,7 @@ export class DataProviderService {
}
// Get items from cache
const itemsToFetch: UniqueAsset[] = [];
const itemsToFetch: AssetProfileIdentifier[] = [];
for (const { dataSource, symbol } of items) {
if (useCache) {
@ -633,7 +633,7 @@ export class DataProviderService {
dataGatheringItems
}: {
currency: string;
dataGatheringItems: UniqueAsset[];
dataGatheringItems: AssetProfileIdentifier[];
}) {
return dataGatheringItems.some(({ dataSource, symbol }) => {
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 marketData = await this.marketDataService.getRange({
dateQuery: { gte: startDate, lt: endDate },
uniqueAssets: [
assetProfileIdentifiers: [
{
dataSource,
symbol
}
]
],
dateQuery: { gte: startDate, lt: endDate }
});
if (marketData?.length > 0) {
@ -392,13 +392,13 @@ export class ExchangeRateDataService {
}
} else {
const marketData = await this.marketDataService.getRange({
dateQuery: { gte: startDate, lt: endDate },
uniqueAssets: [
assetProfileIdentifiers: [
{
dataSource,
symbol: `${DEFAULT_CURRENCY}${currencyFrom}`
}
]
],
dateQuery: { gte: startDate, lt: endDate }
});
for (const { date, marketPrice } of marketData) {
@ -415,16 +415,16 @@ export class ExchangeRateDataService {
}
} else {
const marketData = await this.marketDataService.getRange({
dateQuery: {
gte: startDate,
lt: endDate
},
uniqueAssets: [
assetProfileIdentifiers: [
{
dataSource,
symbol: `${DEFAULT_CURRENCY}${currencyTo}`
}
]
],
dateQuery: {
gte: startDate,
lt: endDate
}
});
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 {
@ -34,6 +37,6 @@ export interface IDataProviderResponse {
marketState: MarketState;
}
export interface IDataGatheringItem extends UniqueAsset {
export interface IDataGatheringItem extends AssetProfileIdentifier {
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 { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { resetHours } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common';
import {
@ -17,7 +17,7 @@ import {
export class MarketDataService {
public constructor(private readonly prismaService: PrismaService) {}
public async deleteMany({ dataSource, symbol }: UniqueAsset) {
public async deleteMany({ dataSource, symbol }: AssetProfileIdentifier) {
return this.prismaService.marketData.deleteMany({
where: {
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({
select: {
date: true,
@ -59,11 +59,11 @@ export class MarketDataService {
}
public async getRange({
dateQuery,
uniqueAssets
assetProfileIdentifiers,
dateQuery
}: {
assetProfileIdentifiers: AssetProfileIdentifier[];
dateQuery: DateQuery;
uniqueAssets: UniqueAsset[];
}): Promise<MarketData[]> {
return this.prismaService.marketData.findMany({
orderBy: [
@ -76,13 +76,13 @@ export class MarketDataService {
],
where: {
dataSource: {
in: uniqueAssets.map(({ dataSource }) => {
in: assetProfileIdentifiers.map(({ dataSource }) => {
return dataSource;
})
},
date: dateQuery,
symbol: {
in: uniqueAssets.map(({ symbol }) => {
in: assetProfileIdentifiers.map(({ 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 { UNKNOWN_KEY } from '@ghostfolio/common/config';
import {
AssetProfileIdentifier,
EnhancedSymbolProfile,
Holding,
ScraperConfiguration,
UniqueAsset
ScraperConfiguration
} from '@ghostfolio/common/interfaces';
import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
@ -23,7 +23,7 @@ export class SymbolProfileService {
return this.prismaService.symbolProfile.create({ data: assetProfile });
}
public async delete({ dataSource, symbol }: UniqueAsset) {
public async delete({ dataSource, symbol }: AssetProfileIdentifier) {
return this.prismaService.symbolProfile.delete({
where: { dataSource_symbol: { dataSource, symbol } }
});
@ -36,7 +36,7 @@ export class SymbolProfileService {
}
public async getSymbolProfiles(
aUniqueAssets: UniqueAsset[]
aAssetProfileIdentifiers: AssetProfileIdentifier[]
): Promise<EnhancedSymbolProfile[]> {
return this.prismaService.symbolProfile
.findMany({
@ -54,7 +54,7 @@ export class SymbolProfileService {
SymbolProfileOverrides: true
},
where: {
OR: aUniqueAssets.map(({ dataSource, symbol }) => {
OR: aAssetProfileIdentifiers.map(({ dataSource, symbol }) => {
return {
dataSource,
symbol
@ -140,7 +140,7 @@ export class SymbolProfileService {
symbolMapping,
SymbolProfileOverrides,
url
}: Prisma.SymbolProfileUpdateInput & UniqueAsset) {
}: AssetProfileIdentifier & Prisma.SymbolProfileUpdateInput) {
return this.prismaService.symbolProfile.update({
data: {
assetClass,

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

@ -7,9 +7,9 @@ import {
} from '@ghostfolio/common/config';
import { getDateFormatString } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
Filter,
InfoItem,
UniqueAsset,
User
} from '@ghostfolio/common/interfaces';
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 });
}
@ -266,21 +266,27 @@ export class AdminMarketDataComponent
.subscribe(() => {});
}
public onGatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
public onGatherProfileDataBySymbol({
dataSource,
symbol
}: AssetProfileIdentifier) {
this.adminService
.gatherProfileDataBySymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {});
}
public onGatherSymbol({ dataSource, symbol }: UniqueAsset) {
public onGatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) {
this.adminService
.gatherSymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {});
}
public onOpenAssetProfileDialog({ dataSource, symbol }: UniqueAsset) {
public onOpenAssetProfileDialog({
dataSource,
symbol
}: AssetProfileIdentifier) {
this.router.navigate([], {
queryParams: {
dataSource,

16
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 { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
import {
AdminMarketDataItem,
UniqueAsset
AssetProfileIdentifier,
AdminMarketDataItem
} from '@ghostfolio/common/interfaces';
import { Injectable } from '@angular/core';
@ -13,7 +13,7 @@ import { EMPTY, catchError, finalize, forkJoin, takeUntil } from 'rxjs';
export class AdminMarketDataService {
public constructor(private adminService: AdminService) {}
public deleteAssetProfile({ dataSource, symbol }: UniqueAsset) {
public deleteAssetProfile({ dataSource, symbol }: AssetProfileIdentifier) {
const confirmation = confirm(
$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(
$localize`Do you really want to delete these profiles?`
);
if (confirmation) {
const deleteRequests = uniqueAssets.map(({ dataSource, symbol }) => {
const deleteRequests = aAssetProfileIdentifiers.map(
({ dataSource, symbol }) => {
return this.adminService.deleteProfileData({ dataSource, symbol });
});
}
);
forkJoin(deleteRequests)
.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 {
AdminMarketDataDetails,
UniqueAsset
AssetProfileIdentifier
} from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n';
@ -175,20 +175,23 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
this.dialogRef.close();
}
public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
public onDeleteProfileData({ dataSource, symbol }: AssetProfileIdentifier) {
this.adminMarketDataService.deleteAssetProfile({ dataSource, symbol });
this.dialogRef.close();
}
public onGatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
public onGatherProfileDataBySymbol({
dataSource,
symbol
}: AssetProfileIdentifier) {
this.adminService
.gatherProfileDataBySymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {});
}
public onGatherSymbol({ dataSource, symbol }: UniqueAsset) {
public onGatherSymbol({ dataSource, symbol }: AssetProfileIdentifier) {
this.adminService
.gatherSymbol({ dataSource, symbol })
.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
.postBenchmark({ dataSource, symbol })
.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
.deleteBenchmark({ dataSource, symbol })
.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 { UserService } from '@ghostfolio/client/services/user/user.service';
import {
AssetProfileIdentifier,
PortfolioPosition,
UniqueAsset,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -108,7 +108,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
this.initialize();
}
public onSymbolClicked({ dataSource, symbol }: UniqueAsset) {
public onSymbolClicked({ dataSource, symbol }: AssetProfileIdentifier) {
if (dataSource && symbol) {
this.router.navigate([], {
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 { NUMERICAL_PRECISION_THRESHOLD } from '@ghostfolio/common/config';
import {
AssetProfileIdentifier,
LineChartItem,
PortfolioPerformance,
UniqueAsset,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -26,7 +26,7 @@ import { takeUntil } from 'rxjs/operators';
export class HomeOverviewComponent implements OnDestroy, OnInit {
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public deviceType: string;
public errors: UniqueAsset[];
public errors: AssetProfileIdentifier[];
public hasError: boolean;
public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean;

9
apps/client/src/app/pages/portfolio/activities/activities-page.component.ts

@ -33,7 +33,6 @@ import { ImportActivitiesDialogParams } from './import-activities-dialog/interfa
templateUrl: './activities-page.html'
})
export class ActivitiesPageComponent implements OnDestroy, OnInit {
public activities: Activity[];
public dataSource: MatTableDataSource<Activity>;
public deviceType: string;
public hasImpersonationId: boolean;
@ -66,13 +65,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
if (params['createDialog']) {
this.openCreateActivityDialog();
} else if (params['editDialog']) {
if (this.activities) {
const activity = this.activities.find(({ id }) => {
return id === params['activityId'];
});
this.openUpdateActivityDialog(activity);
} else if (this.dataSource) {
if (this.dataSource && params['activityId']) {
const activity = this.dataSource.data.find(({ id }) => {
return id === params['activityId'];
});

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 {
public accounts: CreateAccountDto[] = [];
public activities: Activity[] = [];
public assetProfileForm: FormGroup;
public dataSource: MatTableDataSource<Activity>;
public details: any[] = [];
public deviceType: string;
@ -53,7 +54,6 @@ export class ImportActivitiesDialog implements OnDestroy {
public sortDirection: SortDirection = 'desc';
public stepperOrientation: StepperOrientation;
public totalItems: number;
public uniqueAssetForm: FormGroup;
private unsubscribeSubject = new Subject<void>();
@ -73,8 +73,8 @@ export class ImportActivitiesDialog implements OnDestroy {
this.stepperOrientation =
this.deviceType === 'mobile' ? 'vertical' : 'horizontal';
this.uniqueAssetForm = this.formBuilder.group({
uniqueAsset: [undefined, Validators.required]
this.assetProfileForm = this.formBuilder.group({
assetProfileIdentifier: [undefined, Validators.required]
});
if (
@ -85,7 +85,7 @@ export class ImportActivitiesDialog implements OnDestroy {
this.dialogTitle = $localize`Import Dividends`;
this.mode = 'DIVIDEND';
this.uniqueAssetForm.get('uniqueAsset').disable();
this.assetProfileForm.get('assetProfileIdentifier').disable();
this.dataService
.fetchPortfolioHoldings({
@ -102,7 +102,7 @@ export class ImportActivitiesDialog implements OnDestroy {
this.holdings = sortBy(holdings, ({ name }) => {
return name.toLowerCase();
});
this.uniqueAssetForm.get('uniqueAsset').enable();
this.assetProfileForm.get('assetProfileIdentifier').enable();
this.isLoading = false;
@ -167,10 +167,11 @@ export class ImportActivitiesDialog implements OnDestroy {
}
public onLoadDividends(aStepper: MatStepper) {
this.uniqueAssetForm.get('uniqueAsset').disable();
this.assetProfileForm.get('assetProfileIdentifier').disable();
const { dataSource, symbol } =
this.uniqueAssetForm.get('uniqueAsset').value;
const { dataSource, symbol } = this.assetProfileForm.get(
'assetProfileIdentifier'
).value;
this.dataService
.fetchDividendsImport({
@ -193,7 +194,7 @@ export class ImportActivitiesDialog implements OnDestroy {
this.details = [];
this.errorMessages = [];
this.importStep = ImportStep.SELECT_ACTIVITIES;
this.uniqueAssetForm.get('uniqueAsset').enable();
this.assetProfileForm.get('assetProfileIdentifier').enable();
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">
@if (mode === 'DIVIDEND') {
<form
[formGroup]="uniqueAssetForm"
[formGroup]="assetProfileForm"
(ngSubmit)="onLoadDividends(stepper)"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Holding</mat-label>
<mat-select formControlName="uniqueAsset">
<mat-select formControlName="assetProfileIdentifier">
<mat-select-trigger>{{
uniqueAssetForm.get('uniqueAsset')?.value?.name
assetProfileForm.get('assetProfileIdentifier')?.value?.name
}}</mat-select-trigger>
@for (holding of holdings; track holding) {
<mat-option
@ -63,7 +63,7 @@
color="primary"
mat-flat-button
type="submit"
[disabled]="!uniqueAssetForm.valid"
[disabled]="!assetProfileForm.valid"
>
<span i18n>Load Dividends</span>
</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 { prettifySymbol } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
Holding,
PortfolioDetails,
PortfolioPosition,
UniqueAsset,
User
} from '@ghostfolio/common/interfaces';
import { Market, MarketAdvanced } from '@ghostfolio/common/types';
@ -161,7 +161,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.initialize();
}
public onAccountChartClicked({ symbol }: UniqueAsset) {
public onAccountChartClicked({ symbol }: AssetProfileIdentifier) {
if (symbol && symbol !== UNKNOWN_KEY) {
this.router.navigate([], {
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) {
this.router.navigate([], {
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 { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
AdminData,
AdminJobs,
AdminMarketData,
AdminMarketDataDetails,
EnhancedSymbolProfile,
Filter,
UniqueAsset
Filter
} from '@ghostfolio/common/interfaces';
import { HttpClient, HttpParams } from '@angular/common/http';
@ -35,7 +35,7 @@ export class AdminService {
private http: HttpClient
) {}
public addAssetProfile({ dataSource, symbol }: UniqueAsset) {
public addAssetProfile({ dataSource, symbol }: AssetProfileIdentifier) {
return this.http.post<void>(
`/api/v1/admin/profile-data/${dataSource}/${symbol}`,
null
@ -62,7 +62,7 @@ export class AdminService {
return this.http.delete<void>(`/api/v1/platform/${aId}`);
}
public deleteProfileData({ dataSource, symbol }: UniqueAsset) {
public deleteProfileData({ dataSource, symbol }: AssetProfileIdentifier) {
return this.http.delete<void>(
`/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', {});
}
public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
public gatherProfileDataBySymbol({
dataSource,
symbol
}: AssetProfileIdentifier) {
return this.http.post<void>(
`/api/v1/admin/gather/profile-data/${dataSource}/${symbol}`,
{}
@ -178,7 +181,7 @@ export class AdminService {
dataSource,
date,
symbol
}: UniqueAsset & {
}: AssetProfileIdentifier & {
date?: Date;
}) {
let url = `/api/v1/admin/gather/${dataSource}/${symbol}`;
@ -217,7 +220,7 @@ export class AdminService {
symbol,
symbolMapping,
url
}: UniqueAsset & UpdateAssetProfileDto) {
}: AssetProfileIdentifier & UpdateAssetProfileDto) {
return this.http.patch<EnhancedSymbolProfile>(
`/api/v1/admin/profile-data/${dataSource}/${symbol}`,
{
@ -272,7 +275,7 @@ export class AdminService {
dataSource,
scraperConfiguration,
symbol
}: UniqueAsset & UpdateAssetProfileDto['scraperConfiguration']) {
}: AssetProfileIdentifier & UpdateAssetProfileDto['scraperConfiguration']) {
return this.http.post<any>(
`/api/v1/admin/market-data/${dataSource}/${symbol}/test`,
{

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

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

512
apps/client/src/locales/messages.ca.xlf

File diff suppressed because it is too large

486
apps/client/src/locales/messages.de.xlf

File diff suppressed because it is too large

486
apps/client/src/locales/messages.es.xlf

File diff suppressed because it is too large

486
apps/client/src/locales/messages.fr.xlf

File diff suppressed because it is too large

540
apps/client/src/locales/messages.it.xlf

File diff suppressed because it is too large

500
apps/client/src/locales/messages.nl.xlf

File diff suppressed because it is too large

486
apps/client/src/locales/messages.pl.xlf

File diff suppressed because it is too large

486
apps/client/src/locales/messages.pt.xlf

File diff suppressed because it is too large

524
apps/client/src/locales/messages.tr.xlf

File diff suppressed because it is too large

454
apps/client/src/locales/messages.xlf

File diff suppressed because it is too large

486
apps/client/src/locales/messages.zh.xlf

File diff suppressed because it is too large

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

@ -19,7 +19,7 @@ import {
ghostfolioScraperApiSymbolPrefix,
locale
} from './config';
import { Benchmark, UniqueAsset } from './interfaces';
import { AssetProfileIdentifier, Benchmark } from './interfaces';
import { BenchmarkTrend, ColorScheme } from './types';
export const DATE_FORMAT = 'yyyy-MM-dd';
@ -147,7 +147,10 @@ export function getAllActivityTypes(): ActivityType[] {
return Object.values(ActivityType);
}
export function getAssetProfileIdentifier({ dataSource, symbol }: UniqueAsset) {
export function getAssetProfileIdentifier({
dataSource,
symbol
}: AssetProfileIdentifier) {
return `${dataSource}-${symbol}`;
}
@ -377,7 +380,7 @@ export function parseDate(date: string): Date | null {
return parseISO(date);
}
export function parseSymbol({ dataSource, symbol }: UniqueAsset) {
export function parseSymbol({ dataSource, symbol }: AssetProfileIdentifier) {
const [ticker, exchange] = symbol.split('.');
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 {
exchangeRates: ({
label1: string;
label2: string;
value: number;
} & UniqueAsset)[];
} & AssetProfileIdentifier)[];
settings: { [key: string]: boolean | object | string | string[] };
transactionCount: 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';
export interface UniqueAsset {
export interface AssetProfileIdentifier {
dataSource: DataSource;
symbol: string;
}

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

@ -7,6 +7,7 @@ import type {
AdminMarketData,
AdminMarketDataItem
} from './admin-market-data.interface';
import type { AssetProfileIdentifier } from './asset-profile-identifier.interface';
import type { BenchmarkMarketDataDetails } from './benchmark-market-data-details.interface';
import type { BenchmarkProperty } from './benchmark-property.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 { SystemMessage } from './system-message.interface';
import type { TabConfiguration } from './tab-configuration.interface';
import type { UniqueAsset } from './unique-asset.interface';
import type { UserSettings } from './user-settings.interface';
import type { User } from './user.interface';
@ -61,6 +61,7 @@ export {
AdminMarketData,
AdminMarketDataDetails,
AdminMarketDataItem,
AssetProfileIdentifier,
Benchmark,
BenchmarkMarketDataDetails,
BenchmarkProperty,
@ -101,7 +102,6 @@ export {
Subscription,
SymbolMetrics,
TabConfiguration,
UniqueAsset,
User,
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 {
errors?: UniqueAsset[];
errors?: AssetProfileIdentifier[];
hasErrors: boolean;
}

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

@ -1,5 +1,5 @@
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 { Big } from 'big.js';
@ -9,7 +9,7 @@ export class PortfolioSnapshot {
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
currentValueInBaseCurrency: Big;
errors?: UniqueAsset[];
errors?: AssetProfileIdentifier[];
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)

133
libs/common/src/lib/personal-finance-tools.ts

@ -15,7 +15,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'allvue-systems',
name: 'Allvue Systems',
origin: `United States`,
origin: 'United States',
slogan: 'Investment Software Suite'
},
{
@ -30,7 +30,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'altoo',
name: 'Altoo Wealth Platform',
origin: `Switzerland`,
origin: 'Switzerland',
slogan: 'Simplicity for Complex Wealth'
},
{
@ -40,7 +40,7 @@ export const personalFinanceTools: Product[] = [
key: 'anlage.app',
languages: ['English'],
name: 'Anlage.App',
origin: `Austria`,
origin: 'Austria',
pricingPerYear: '$120',
slogan: 'Analyze and track your portfolio.'
},
@ -58,16 +58,27 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'beanvest',
name: 'Beanvest',
origin: `France`,
origin: 'France',
pricingPerYear: '$100',
slogan: 'Stock Portfolio Tracker for Smart Investors'
},
{
founded: 2022,
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'degiro-portfolio-tracker-by-capitalyse',
languages: ['English'],
name: 'DEGIRO Portfolio Tracker by Capitalyse',
origin: 'Netherlands',
pricingPerYear: '€24',
slogan: 'Democratizing Data Analytics'
},
{
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'capitally',
name: 'Capitally',
origin: `Poland`,
origin: 'Poland',
pricingPerYear: '€50',
slogan: 'Optimize your investments performance'
},
@ -75,7 +86,7 @@ export const personalFinanceTools: Product[] = [
founded: 2022,
key: 'capmon',
name: 'CapMon.org',
origin: `Germany`,
origin: 'Germany',
note: 'CapMon.org was discontinued in 2023',
slogan: 'Next Generation Assets Tracking'
},
@ -83,7 +94,7 @@ export const personalFinanceTools: Product[] = [
founded: 2019,
key: 'compound-planning',
name: 'Compound Planning',
origin: `United States`,
origin: 'United States',
slogan: 'Modern Wealth & Investment Management'
},
{
@ -92,7 +103,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'copilot-money',
name: 'Copilot Money',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$70',
slogan: 'Do money better with Copilot'
},
@ -110,7 +121,7 @@ export const personalFinanceTools: Product[] = [
key: 'delta',
name: 'Delta Investment Tracker',
note: 'Acquired by eToro',
origin: `Belgium`,
origin: 'Belgium',
slogan: 'The app to track all your investments. Make smart moves only.'
},
{
@ -120,7 +131,7 @@ export const personalFinanceTools: Product[] = [
key: 'divvydiary',
languages: ['Deutsch', 'English'],
name: 'DivvyDiary',
origin: `Germany`,
origin: 'Germany',
pricingPerYear: '€65',
slogan: 'Your personal Dividend Calendar'
},
@ -130,7 +141,7 @@ export const personalFinanceTools: Product[] = [
key: 'empower',
name: 'Empower',
note: 'Originally named as Personal Capital',
origin: `United States`,
origin: 'United States',
slogan: 'Get answers to your money questions'
},
{
@ -138,7 +149,7 @@ export const personalFinanceTools: Product[] = [
founded: 2022,
key: 'eightfigures',
name: '8FIGURES',
origin: `United States`,
origin: 'United States',
slogan: 'Portfolio Tracker Designed by Professional Investors'
},
{
@ -147,7 +158,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'exirio',
name: 'Exirio',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$100',
slogan: 'All your wealth, in one place.'
},
@ -158,7 +169,7 @@ export const personalFinanceTools: Product[] = [
key: 'fina',
languages: ['English'],
name: 'Fina',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$115',
slogan: 'Flexible Financial Management'
},
@ -167,7 +178,7 @@ export const personalFinanceTools: Product[] = [
key: 'finary',
languages: ['Deutsch', 'English', 'Français'],
name: 'Finary',
origin: `United States`,
origin: 'United States',
slogan: 'Real-Time Portfolio Tracker & Stock Tracker'
},
{
@ -175,7 +186,7 @@ export const personalFinanceTools: Product[] = [
hasFreePlan: true,
key: 'finwise',
name: 'FinWise',
origin: `South Africa`,
origin: 'South Africa',
pricingPerYear: '€69.99',
slogan: 'Personal finances, simplified'
},
@ -185,7 +196,7 @@ export const personalFinanceTools: Product[] = [
key: 'folishare',
languages: ['Deutsch', 'English'],
name: 'folishare',
origin: `Austria`,
origin: 'Austria',
pricingPerYear: '$65',
slogan: 'Take control over your investments'
},
@ -196,7 +207,7 @@ export const personalFinanceTools: Product[] = [
key: 'getquin',
languages: ['Deutsch', 'English'],
name: 'getquin',
origin: `Germany`,
origin: 'Germany',
pricingPerYear: '€48',
slogan: 'Portfolio Tracker, Analysis & Community'
},
@ -205,7 +216,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'gospatz',
name: 'goSPATZ',
origin: `Germany`,
origin: 'Germany',
slogan: 'Volle Kontrolle über deine Investitionen'
},
{
@ -214,7 +225,7 @@ export const personalFinanceTools: Product[] = [
key: 'holistic-capital',
languages: ['Deutsch'],
name: 'Holistic',
origin: `Germany`,
origin: 'Germany',
slogan: 'Die All-in-One Lösung für dein Vermögen.',
useAnonymously: true
},
@ -224,7 +235,7 @@ export const personalFinanceTools: Product[] = [
key: 'intuit-mint',
name: 'Intuit Mint',
note: 'Intuit Mint was discontinued in 2023',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$60',
slogan: 'Managing money, made simple'
},
@ -234,7 +245,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'justetf',
name: 'justETF',
origin: `Germany`,
origin: 'Germany',
pricingPerYear: '€119',
slogan: 'ETF portfolios made simple'
},
@ -244,7 +255,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'koyfin',
name: 'Koyfin',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$468',
slogan: 'Comprehensive financial data analysis'
},
@ -254,7 +265,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'kubera',
name: 'Kubera®',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$150',
slogan: 'The Time Machine for your Net Worth'
},
@ -264,7 +275,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'magnifi',
name: 'Magnifi',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$132',
slogan: 'AI Investing Assistant'
},
@ -275,9 +286,9 @@ export const personalFinanceTools: Product[] = [
key: 'markets.sh',
languages: ['English'],
name: 'markets.sh',
origin: `Germany`,
origin: 'Germany',
pricingPerYear: '€168',
regions: [`Global`],
regions: ['Global'],
slogan: 'Track your investments'
},
{
@ -287,9 +298,9 @@ export const personalFinanceTools: Product[] = [
languages: ['English'],
name: 'Maybe Finance',
note: 'Maybe Finance was discontinued in 2023',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$145',
regions: [`United States`],
regions: ['United States'],
slogan: 'Your financial future, in your control'
},
{
@ -298,7 +309,7 @@ export const personalFinanceTools: Product[] = [
key: 'merlincrypto',
languages: ['English'],
name: 'Merlin',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$204',
regions: ['Canada', 'United States'],
slogan: 'The smartest way to track your crypto'
@ -309,7 +320,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'monarch-money',
name: 'Monarch Money',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$99.99',
slogan: 'The modern way to manage your money'
},
@ -327,7 +338,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'navexa',
name: 'Navexa',
origin: `Australia`,
origin: 'Australia',
pricingPerYear: '$90',
slogan: 'The Intelligent Portfolio Tracker'
},
@ -338,7 +349,7 @@ export const personalFinanceTools: Product[] = [
key: 'parqet',
name: 'Parqet',
note: 'Originally named as Tresor One',
origin: `Germany`,
origin: 'Germany',
pricingPerYear: '€88',
regions: ['Austria', 'Germany', 'Switzerland'],
slogan: 'Dein Vermögen immer im Blick'
@ -348,7 +359,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'plannix',
name: 'Plannix',
origin: `Italy`,
origin: 'Italy',
slogan: 'Your Personal Finance Hub'
},
{
@ -358,9 +369,9 @@ export const personalFinanceTools: Product[] = [
key: 'pocketsmith',
languages: ['English'],
name: 'PocketSmith',
origin: `New Zealand`,
origin: 'New Zealand',
pricingPerYear: '$120',
regions: [`Global`],
regions: ['Global'],
slogan: 'Know where your money is going'
},
{
@ -369,7 +380,7 @@ export const personalFinanceTools: Product[] = [
key: 'portfolio-dividend-tracker',
languages: ['English', 'Nederlands'],
name: 'Portfolio Dividend Tracker',
origin: `Netherlands`,
origin: 'Netherlands',
pricingPerYear: '€60',
slogan: 'Manage all your portfolios'
},
@ -397,7 +408,7 @@ export const personalFinanceTools: Product[] = [
key: 'portseido',
languages: ['Deutsch', 'English', 'Français', 'Nederlands'],
name: 'Portseido',
origin: `Thailand`,
origin: 'Thailand',
pricingPerYear: '$96',
slogan: 'Portfolio Performance and Dividend Tracker'
},
@ -407,7 +418,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: true,
key: 'projectionlab',
name: 'ProjectionLab',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$108',
slogan: 'Build Financial Plans You Love.'
},
@ -416,7 +427,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'rocket-money',
name: 'Rocket Money',
origin: `United States`,
origin: 'United States',
slogan: 'Track your net worth'
},
{
@ -425,7 +436,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'seeking-alpha',
name: 'Seeking Alpha',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$239',
slogan: 'Stock Market Analysis & Tools for Investors'
},
@ -433,7 +444,7 @@ export const personalFinanceTools: Product[] = [
founded: 2022,
key: 'segmio',
name: 'Segmio',
origin: `Romania`,
origin: 'Romania',
slogan: 'Wealth Management and Net Worth Tracking'
},
{
@ -442,9 +453,9 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'sharesight',
name: 'Sharesight',
origin: `New Zealand`,
origin: 'New Zealand',
pricingPerYear: '$135',
regions: [`Global`],
regions: ['Global'],
slogan: 'Stock Portfolio Tracker'
},
{
@ -459,7 +470,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'simple-portfolio',
name: 'Simple Portfolio',
origin: `Czech Republic`,
origin: 'Czech Republic',
pricingPerYear: '€80',
slogan: 'Stock Portfolio Tracker'
},
@ -469,7 +480,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'snowball-analytics',
name: 'Snowball Analytics',
origin: `France`,
origin: 'France',
pricingPerYear: '$80',
slogan: 'Simple and powerful portfolio tracker'
},
@ -478,20 +489,20 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'stock-events',
name: 'Stock Events',
origin: `Germany`,
origin: 'Germany',
slogan: 'Track all your Investments'
},
{
key: 'stockle',
name: 'Stockle',
origin: `Finland`,
origin: 'Finland',
slogan: 'Supercharge your investments tracking experience'
},
{
founded: 2008,
key: 'stockmarketeye',
name: 'StockMarketEye',
origin: `France`,
origin: 'France',
note: 'StockMarketEye was discontinued in 2023',
slogan: 'A Powerful Portfolio & Investment Tracking App'
},
@ -501,7 +512,7 @@ export const personalFinanceTools: Product[] = [
key: 'stonksfolio',
languages: ['English'],
name: 'Stonksfolio',
origin: `Bulgaria`,
origin: 'Bulgaria',
pricingPerYear: '€49.90',
slogan: 'Visualize all of your portfolios'
},
@ -510,7 +521,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'sumio',
name: 'Sumio',
origin: `Czech Republic`,
origin: 'Czech Republic',
pricingPerYear: '$20',
slogan: 'Sum up and build your wealth.'
},
@ -519,7 +530,7 @@ export const personalFinanceTools: Product[] = [
hasFreePlan: false,
key: 'tiller',
name: 'Tiller',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$79',
slogan:
'Your financial life in a spreadsheet, automatically updated each day'
@ -530,7 +541,7 @@ export const personalFinanceTools: Product[] = [
key: 'utluna',
languages: ['Deutsch', 'English', 'Français'],
name: 'Utluna',
origin: `Switzerland`,
origin: 'Switzerland',
pricingPerYear: '$300',
slogan: 'Your Portfolio. Revealed.',
useAnonymously: true
@ -540,7 +551,7 @@ export const personalFinanceTools: Product[] = [
hasFreePlan: true,
key: 'vyzer',
name: 'Vyzer',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$348',
slogan: 'Virtual Family Office for Smart Wealth Management'
},
@ -549,7 +560,7 @@ export const personalFinanceTools: Product[] = [
key: 'wallmine',
languages: ['English'],
name: 'wallmine',
origin: `Czech Republic`,
origin: 'Czech Republic',
pricingPerYear: '$600',
slogan: 'Make Smarter Investments'
},
@ -567,7 +578,7 @@ export const personalFinanceTools: Product[] = [
key: 'wealthica',
languages: ['English', 'Français'],
name: 'Wealthica',
origin: `Canada`,
origin: 'Canada',
pricingPerYear: '$50',
slogan: 'See all your investments in one place'
},
@ -577,13 +588,13 @@ export const personalFinanceTools: Product[] = [
key: 'wealthy-tracker',
languages: ['English'],
name: 'Wealthy Tracker',
origin: `India`,
origin: 'India',
slogan: 'One app to manage all your investments'
},
{
key: 'whal',
name: 'Whal',
origin: `United States`,
origin: 'United States',
slogan: 'Manage your investments in one place'
},
{
@ -594,8 +605,8 @@ export const personalFinanceTools: Product[] = [
languages: ['Deutsch', 'English', 'Español', 'Français', 'Italiano'],
name: 'yeekatee',
note: 'yeekatee was discontinued in 2024',
origin: `Switzerland`,
regions: [`Global`],
origin: 'Switzerland',
regions: ['Global'],
slogan: 'Connect. Share. Invest.'
},
{
@ -604,7 +615,7 @@ export const personalFinanceTools: Product[] = [
hasSelfHostingAbility: false,
key: 'ynab',
name: 'YNAB (You Need a Budget)',
origin: `United States`,
origin: 'United States',
pricingPerYear: '$99',
slogan: 'Change Your Relationship With Money'
}

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 { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
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 { GfActivityTypeComponent } from '@ghostfolio/ui/activity-type';
import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info';
@ -99,7 +99,7 @@ export class GfActivitiesTableComponent
@Output() export = new EventEmitter<void>();
@Output() exportDrafts = new EventEmitter<string[]>();
@Output() import = new EventEmitter<void>();
@Output() importDividends = new EventEmitter<UniqueAsset>();
@Output() importDividends = new EventEmitter<AssetProfileIdentifier>();
@Output() pageChanged = new EventEmitter<PageEvent>();
@Output() selectedActivities = new EventEmitter<Activity[]>();
@Output() sortChanged = new EventEmitter<Sort>();
@ -263,7 +263,7 @@ export class GfActivitiesTableComponent
alert(aComment);
}
public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset) {
public onOpenPositionDialog({ dataSource, symbol }: AssetProfileIdentifier) {
this.router.navigate([], {
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';
export interface IDateRangeOption {
@ -6,7 +6,7 @@ export interface IDateRangeOption {
value: DateRange;
}
export interface ISearchResultItem extends UniqueAsset {
export interface ISearchResultItem extends AssetProfileIdentifier {
assetSubClassString: string;
currency: string;
name: string;

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

@ -1,5 +1,9 @@
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 { GfTrendIndicatorComponent } from '@ghostfolio/ui/trend-indicator';
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([], {
queryParams: { dataSource, symbol, benchmarkDetailDialog: true }
});
@ -95,7 +99,10 @@ export class GfBenchmarkComponent implements OnChanges, OnDestroy {
this.unsubscribeSubject.complete();
}
private openBenchmarkDetailDialog({ dataSource, symbol }: UniqueAsset) {
private openBenchmarkDetailDialog({
dataSource,
symbol
}: AssetProfileIdentifier) {
const dialogRef = this.dialog.open(GfBenchmarkDetailDialogComponent, {
data: <BenchmarkDetailDialogParams>{
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 { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
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 { 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) {
this.router.navigate([], {
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 { UNKNOWN_KEY } from '@ghostfolio/common/config';
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 { 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>;

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

@ -1,5 +1,8 @@
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 {
@ -40,7 +43,7 @@ export class GfTreemapChartComponent
@Input() cursor: string;
@Input() holdings: PortfolioPosition[];
@Output() treemapChartClicked = new EventEmitter<UniqueAsset>();
@Output() treemapChartClicked = new EventEmitter<AssetProfileIdentifier>();
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;

882
package-lock.json

File diff suppressed because it is too large

22
package.json

@ -152,16 +152,16 @@
"@angular/pwa": "18.1.1",
"@nestjs/schematics": "10.0.1",
"@nestjs/testing": "10.1.3",
"@nx/angular": "19.5.1",
"@nx/cypress": "19.5.1",
"@nx/eslint-plugin": "19.5.1",
"@nx/jest": "19.5.1",
"@nx/js": "19.5.1",
"@nx/nest": "19.5.1",
"@nx/node": "19.5.1",
"@nx/storybook": "19.5.1",
"@nx/web": "19.5.1",
"@nx/workspace": "19.5.1",
"@nx/angular": "19.5.6",
"@nx/cypress": "19.5.6",
"@nx/eslint-plugin": "19.5.6",
"@nx/jest": "19.5.6",
"@nx/js": "19.5.6",
"@nx/nest": "19.5.6",
"@nx/node": "19.5.6",
"@nx/storybook": "19.5.6",
"@nx/web": "19.5.6",
"@nx/workspace": "19.5.6",
"@schematics/angular": "18.1.1",
"@simplewebauthn/types": "9.0.1",
"@storybook/addon-essentials": "8.2.6",
@ -190,7 +190,7 @@
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jest-preset-angular": "14.1.0",
"nx": "19.5.1",
"nx": "19.5.6",
"prettier": "3.3.3",
"prettier-plugin-organize-attributes": "1.0.0",
"react": "18.2.0",

Loading…
Cancel
Save