From cb34c4bc5a9023f4e7f7e7a9d00ea7a50bf0b2f3 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Fri, 19 Jun 2026 18:30:21 +0200
Subject: [PATCH 1/7] Task/extend personal finance tools (20260619) (#7071)
Extend personal finance tools
---
libs/common/src/lib/personal-finance-tools.ts | 79 +++++++++++++++++++
1 file changed, 79 insertions(+)
diff --git a/libs/common/src/lib/personal-finance-tools.ts b/libs/common/src/lib/personal-finance-tools.ts
index 8698c76f7..744081464 100644
--- a/libs/common/src/lib/personal-finance-tools.ts
+++ b/libs/common/src/lib/personal-finance-tools.ts
@@ -1,6 +1,17 @@
import { Product } from '@ghostfolio/common/interfaces';
export const personalFinanceTools: Product[] = [
+ {
+ hasFreePlan: true,
+ hasSelfHostingAbility: true,
+ key: 'acemoney',
+ name: 'AceMoney',
+ note: 'License is a perpetual license',
+ origin: 'US',
+ pricingPerYear: '$44.95',
+ slogan: 'Lifetime Personal Finance Control, One Single Payment.',
+ url: 'https://www.mechcad.net'
+ },
{
founded: 2023,
hasSelfHostingAbility: false,
@@ -361,6 +372,16 @@ export const personalFinanceTools: Product[] = [
slogan: 'Your financial superpower',
url: 'https://www.etops.com'
},
+ {
+ founded: 2015,
+ hasFreePlan: true,
+ hasSelfHostingAbility: false,
+ key: 'everydollar',
+ name: 'EveryDollar',
+ origin: 'US',
+ slogan: 'Plan, track, find more margin',
+ url: 'https://www.ramseysolutions.com/money/everydollar'
+ },
{
founded: 2020,
hasFreePlan: true,
@@ -497,6 +518,17 @@ export const personalFinanceTools: Product[] = [
slogan: 'Take control over your investments',
url: 'https://www.folishare.com'
},
+ {
+ founded: 1993,
+ hasSelfHostingAbility: true,
+ key: 'fund-manager',
+ name: 'Fund Manager',
+ note: 'License is a perpetual license',
+ origin: 'US',
+ pricingPerYear: '$99',
+ slogan: 'Powerful portfolio management software',
+ url: 'https://www.fundmanagersoftware.com'
+ },
{
hasFreePlan: true,
hasSelfHostingAbility: false,
@@ -528,6 +560,15 @@ export const personalFinanceTools: Product[] = [
slogan: 'Portfolio Tracker, Analysis & Community',
url: 'https://www.getquin.com'
},
+ {
+ hasFreePlan: true,
+ hasSelfHostingAbility: false,
+ key: 'goodbudget',
+ name: 'Goodbudget',
+ origin: 'US',
+ slogan: 'Budget with a why',
+ url: 'https://goodbudget.com'
+ },
{
hasFreePlan: true,
hasSelfHostingAbility: false,
@@ -665,6 +706,16 @@ export const personalFinanceTools: Product[] = [
slogan: 'Sustainability insights for wealth managers',
url: 'https://leafs.ch'
},
+ {
+ founded: 2020,
+ hasFreePlan: false,
+ hasSelfHostingAbility: false,
+ key: 'lunch-money',
+ name: 'Lunch Money',
+ origin: 'CA',
+ slogan: 'Delightfully simple personal finance and budgeting',
+ url: 'https://lunchmoney.app'
+ },
{
founded: 2018,
hasFreePlan: false,
@@ -725,6 +776,16 @@ export const personalFinanceTools: Product[] = [
slogan: 'The smartest way to track your crypto',
url: 'https://www.merlincrypto.com'
},
+ {
+ hasFreePlan: false,
+ hasSelfHostingAbility: false,
+ key: 'mezzi',
+ name: 'Mezzi',
+ origin: 'US',
+ pricingPerYear: '$48',
+ slogan: 'Self-manage your wealth. Get fiduciary advice.',
+ url: 'https://www.mezzi.com'
+ },
{
founded: 1991,
hasSelfHostingAbility: true,
@@ -776,6 +837,15 @@ export const personalFinanceTools: Product[] = [
slogan: 'Have total control of your financial life',
url: 'https://www.moneyspire.com'
},
+ {
+ hasFreePlan: true,
+ hasSelfHostingAbility: true,
+ key: 'moneywell',
+ name: 'MoneyWell',
+ origin: 'US',
+ slogan: 'Personal Budgeting Software for Mac and iOS',
+ url: 'https://moneywell.app'
+ },
{
key: 'moneywiz',
name: 'MoneyWiz',
@@ -1109,6 +1179,15 @@ export const personalFinanceTools: Product[] = [
slogan: 'Stock Portfolio Tracker',
url: 'https://simpleportfolio.app'
},
+ {
+ founded: 2020,
+ hasFreePlan: false,
+ hasSelfHostingAbility: false,
+ key: 'simplifi',
+ name: 'Simplifi by Quicken',
+ origin: 'US',
+ url: 'https://www.quicken.com/products/simplifi'
+ },
{
founded: 2014,
hasFreePlan: true,
From 2589a60f288fbeb4a2ed3d4d44dc58f56ce9fca3 Mon Sep 17 00:00:00 2001
From: David Requeno <108202767+DavidReque@users.noreply.github.com>
Date: Fri, 19 Jun 2026 10:32:32 -0600
Subject: [PATCH 2/7] Task/include selected profile count in market data delete
action (#7064)
* Include selected profile count
* Update changelog
---
CHANGELOG.md | 4 ++++
.../admin-market-data/admin-market-data.html | 13 ++++++++++++-
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f964c9cae..0a2337d89 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
+### Changed
+
+- Added the selected asset profile count to the delete menu item of the historical market data table in the admin control panel
+
### Fixed
- Fixed an issue with the localization of the country names
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html
index 63d425513..e86e3e631 100644
--- a/apps/client/src/app/components/admin-market-data/admin-market-data.html
+++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html
@@ -239,7 +239,18 @@
[disabled]="!selection.hasValue()"
(click)="onDeleteAssetProfiles()"
>
- Delete Profiles
+ Delete
+ {{
+ selection.selected.length > 1
+ ? selection.selected.length
+ : ''
+ }}
+ {selection.selected.length, plural,
+ =1 {Profile}
+ other {Profiles}
+ }
From 77efbc5a437b5be665b83467f5fab5b9366b36d8 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Fri, 19 Jun 2026 18:35:29 +0200
Subject: [PATCH 3/7] Task/refactor default locale (#7066)
Refactor default locale
---
apps/api/src/app/user/user.service.ts | 6 +++---
.../admin-market-data/admin-market-data.component.ts | 8 ++++----
.../app/components/admin-users/admin-users.component.ts | 4 ++--
.../components/home-watchlist/home-watchlist.component.ts | 4 ++--
.../create-or-update-activity-dialog.component.ts | 5 ++---
libs/common/src/lib/config.ts | 3 +--
libs/common/src/lib/helper.ts | 8 ++++----
.../fire-calculator/fire-calculator.component.stories.ts | 4 ++--
8 files changed, 20 insertions(+), 22 deletions(-)
diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts
index 6d6041c81..0c159bc1c 100644
--- a/apps/api/src/app/user/user.service.ts
+++ b/apps/api/src/app/user/user.service.ts
@@ -28,11 +28,11 @@ import {
DEFAULT_CURRENCY,
DEFAULT_DATE_RANGE,
DEFAULT_LANGUAGE_CODE,
+ DEFAULT_LOCALE,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_REFERRAL_PARTNERS,
PROPERTY_SYSTEM_MESSAGE,
- TAG_ID_EXCLUDE_FROM_ANALYSIS,
- locale as defaultLocale
+ TAG_ID_EXCLUDE_FROM_ANALYSIS
} from '@ghostfolio/common/config';
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
@@ -102,7 +102,7 @@ export class UserService {
public async getUser({
impersonationUserId,
- locale = defaultLocale,
+ locale = DEFAULT_LOCALE,
user
}: {
impersonationUserId: string;
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
index 805adf89d..a4a9b6e19 100644
--- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
+++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
@@ -1,8 +1,8 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
DEFAULT_COLOR_SCHEME,
- DEFAULT_PAGE_SIZE,
- locale
+ DEFAULT_LOCALE,
+ DEFAULT_PAGE_SIZE
} from '@ghostfolio/common/config';
import {
canDeleteAssetProfile,
@@ -429,7 +429,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit {
colorScheme:
this.user?.settings.colorScheme ?? DEFAULT_COLOR_SCHEME,
deviceType: this.deviceType(),
- locale: this.user?.settings?.locale ?? locale
+ locale: this.user?.settings?.locale ?? DEFAULT_LOCALE
} satisfies AssetProfileDialogParams,
height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
@@ -464,7 +464,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit {
autoFocus: false,
data: {
deviceType: this.deviceType(),
- locale: this.user?.settings?.locale ?? locale
+ locale: this.user?.settings?.locale ?? DEFAULT_LOCALE
} satisfies CreateAssetProfileDialogParams,
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts
index f477776a5..15133b781 100644
--- a/apps/client/src/app/components/admin-users/admin-users.component.ts
+++ b/apps/client/src/app/components/admin-users/admin-users.component.ts
@@ -5,7 +5,7 @@ import {
import { GfUserDetailDialogComponent } from '@ghostfolio/client/components/user-detail-dialog/user-detail-dialog.component';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
-import { DEFAULT_PAGE_SIZE, locale } from '@ghostfolio/common/config';
+import { DEFAULT_LOCALE, DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import {
getDateFnsLocale,
@@ -317,7 +317,7 @@ export class GfAdminUsersComponent implements OnInit {
currentUserId: this.user?.id,
deviceType: this.deviceType(),
hasPermissionForSubscription: this.hasPermissionForSubscription,
- locale: this.user?.settings?.locale ?? locale,
+ locale: this.user?.settings?.locale ?? DEFAULT_LOCALE,
userId: aUserId
} satisfies UserDetailDialogParams,
height: this.deviceType() === 'mobile' ? '98vh' : '60vh',
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
index 22d829daa..0107f36a2 100644
--- a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
@@ -1,6 +1,6 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
-import { locale as defaultLocale } from '@ghostfolio/common/config';
+import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
import {
AssetProfileIdentifier,
Benchmark,
@@ -149,7 +149,7 @@ export class GfHomeWatchlistComponent implements OnInit {
autoFocus: false,
data: {
deviceType: this.deviceType(),
- locale: this.user?.settings?.locale ?? defaultLocale
+ locale: this.user?.settings?.locale ?? DEFAULT_LOCALE
},
width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
});
diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
index decb30682..ba17dbef3 100644
--- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
+++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
@@ -1,6 +1,5 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
-import { ASSET_CLASS_MAPPING } from '@ghostfolio/common/config';
-import { locale as defaultLocale } from '@ghostfolio/common/config';
+import { ASSET_CLASS_MAPPING, DEFAULT_LOCALE } from '@ghostfolio/common/config';
import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos';
import { getDateFormatString } from '@ghostfolio/common/helper';
import {
@@ -120,7 +119,7 @@ export class GfCreateOrUpdateActivityDialogComponent {
this.hasPermissionToCreateOwnTag =
this.data.user?.settings?.isExperimentalFeatures &&
hasPermission(this.data.user?.permissions, permissions.createOwnTag);
- this.locale = this.data.user.settings.locale ?? defaultLocale;
+ this.locale = this.data.user.settings.locale ?? DEFAULT_LOCALE;
this.mode = this.data.activity?.id ? 'update' : 'create';
this.dateAdapter.setLocale(this.locale);
diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts
index 3e7fbe6df..4924aaeea 100644
--- a/libs/common/src/lib/config.ts
+++ b/libs/common/src/lib/config.ts
@@ -13,8 +13,6 @@ export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPr
export const ghostfolioFearAndGreedIndexSymbolCryptocurrencies = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_CRYPTOCURRENCIES`;
export const ghostfolioFearAndGreedIndexSymbolStocks = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_STOCKS`;
-export const locale = 'en-US';
-
export const primaryColorHex = '#36cfcc';
export const primaryColorRgb = {
r: 54,
@@ -85,6 +83,7 @@ export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
export const DEFAULT_DATE_RANGE: DateRange = 'max';
export const DEFAULT_HOST = '0.0.0.0';
export const DEFAULT_LANGUAGE_CODE = 'en';
+export const DEFAULT_LOCALE = 'en-US';
export const DEFAULT_PAGE_SIZE = 50;
export const DEFAULT_PORT = 3333;
export const DEFAULT_PROCESSOR_GATHER_ASSET_PROFILE_CONCURRENCY = 1;
diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts
index a29129f91..3ca95d9f6 100644
--- a/libs/common/src/lib/helper.ts
+++ b/libs/common/src/lib/helper.ts
@@ -36,12 +36,12 @@ import { get, isNil, isString } from 'lodash';
import {
DEFAULT_CURRENCY,
+ DEFAULT_LOCALE,
DERIVED_CURRENCIES,
ghostfolioFearAndGreedIndexSymbol,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks,
- ghostfolioScraperApiSymbolPrefix,
- locale
+ ghostfolioScraperApiSymbolPrefix
} from './config';
import {
AdminMarketDataItem,
@@ -261,7 +261,7 @@ export function getCurrencyFromSymbol(aSymbol = '') {
export function getCountryName({ code }: { code: string }): string {
try {
return (
- new Intl.DisplayNames([document.documentElement.lang || locale], {
+ new Intl.DisplayNames([document.documentElement.lang || DEFAULT_LOCALE], {
type: 'region'
}).of(code) ?? code
);
@@ -338,7 +338,7 @@ export function getEmojiFlag(aCountryCode: string) {
}
export function getLocale() {
- return navigator.language ?? locale;
+ return navigator.language ?? DEFAULT_LOCALE;
}
export function getLowercase(object: object, path: string) {
diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts
index 0872c2aac..ad80499d7 100644
--- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts
+++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.stories.ts
@@ -1,4 +1,4 @@
-import { locale } from '@ghostfolio/common/config';
+import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -47,7 +47,7 @@ export const Simple: Story = {
annualInterestRate: 5,
currency: 'USD',
fireWealth: 50000,
- locale: locale,
+ locale: DEFAULT_LOCALE,
savingsRate: 1000
}
};
From 2fe863726198f0f1c6868bd320e2bc34d7a2db64 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Fri, 19 Jun 2026 18:44:29 +0200
Subject: [PATCH 4/7] Task/add selected asset profile count to deletion
confirmation dialog of historical market data table in admin control panel
(#7072)
* Add selected asset profile count
* Update changelog
---
CHANGELOG.md | 1 +
.../admin-market-data/admin-market-data.service.ts | 12 ++++++++++--
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a2337d89..85d661e84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Added the selected asset profile count to the delete menu item of the historical market data table in the admin control panel
+- Added the selected asset profile count to the deletion confirmation dialog of the historical market data table in the admin control panel
### Fixed
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
index 77e2151ef..28c8c2d9f 100644
--- a/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
+++ b/apps/client/src/app/components/admin-market-data/admin-market-data.service.ts
@@ -32,6 +32,8 @@ export class AdminMarketDataService {
public deleteAssetProfiles(
aAssetProfileIdentifiers: AssetProfileIdentifier[]
) {
+ const assetProfileCount = aAssetProfileIdentifiers.length;
+
this.notificationService.confirm({
confirmFn: () => {
const deleteRequests = aAssetProfileIdentifiers.map(
@@ -44,7 +46,10 @@ export class AdminMarketDataService {
.pipe(
catchError(() => {
this.notificationService.alert({
- title: $localize`Oops! Could not delete profiles.`
+ title:
+ assetProfileCount === 1
+ ? $localize`Oops! Could not delete the asset profile.`
+ : $localize`Oops! Could not delete the asset profiles.`
});
return EMPTY;
@@ -56,7 +61,10 @@ export class AdminMarketDataService {
.subscribe();
},
confirmType: ConfirmationDialogType.Warn,
- title: $localize`Do you really want to delete these profiles?`
+ title:
+ assetProfileCount === 1
+ ? $localize`Do you really want to delete this asset profile?`
+ : $localize`Do you really want to delete these ${assetProfileCount}:count: asset profiles?`
});
}
}
From a8e9ae81e07bf3e55def1c3033d602b9d6b055ec Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Fri, 19 Jun 2026 19:05:11 +0200
Subject: [PATCH 5/7] Task/improve sorting to be case-insensitive in platform
and tag management of admin control panel (#7061)
* Improve sorting to be case-insensitive
* Update changelog
---
CHANGELOG.md | 2 ++
.../components/admin-platform/admin-platform.component.ts | 5 ++---
.../src/app/components/admin-tag/admin-tag.component.ts | 5 ++---
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85d661e84..62c6c83f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added the selected asset profile count to the delete menu item of the historical market data table in the admin control panel
- Added the selected asset profile count to the deletion confirmation dialog of the historical market data table in the admin control panel
+- Improved the sorting to be case-insensitive in the platform management of the admin control panel
+- Improved the sorting to be case-insensitive in the tag management of the admin control panel
### Fixed
diff --git a/apps/client/src/app/components/admin-platform/admin-platform.component.ts b/apps/client/src/app/components/admin-platform/admin-platform.component.ts
index a9d135068..26e6c2b1e 100644
--- a/apps/client/src/app/components/admin-platform/admin-platform.component.ts
+++ b/apps/client/src/app/components/admin-platform/admin-platform.component.ts
@@ -1,7 +1,7 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreatePlatformDto, UpdatePlatformDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
-import { getLocale } from '@ghostfolio/common/helper';
+import { getLocale, getLowercase } from '@ghostfolio/common/helper';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { AdminService, DataService } from '@ghostfolio/ui/services';
@@ -33,7 +33,6 @@ import {
ellipsisHorizontal,
trashOutline
} from 'ionicons/icons';
-import { get } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector';
import { GfCreateOrUpdatePlatformDialogComponent } from './create-or-update-platform-dialog/create-or-update-platform-dialog.component';
@@ -147,7 +146,7 @@ export class GfAdminPlatformComponent implements OnInit {
this.dataSource = new MatTableDataSource(platforms);
this.dataSource.sort = this.sort();
- this.dataSource.sortingDataAccessor = get;
+ this.dataSource.sortingDataAccessor = getLowercase;
this.dataService.updateInfo();
diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.ts b/apps/client/src/app/components/admin-tag/admin-tag.component.ts
index c00fe3a61..fcb901707 100644
--- a/apps/client/src/app/components/admin-tag/admin-tag.component.ts
+++ b/apps/client/src/app/components/admin-tag/admin-tag.component.ts
@@ -1,7 +1,7 @@
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateTagDto, UpdateTagDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
-import { getLocale } from '@ghostfolio/common/helper';
+import { getLocale, getLowercase } from '@ghostfolio/common/helper';
import { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value';
@@ -32,7 +32,6 @@ import {
ellipsisHorizontal,
trashOutline
} from 'ionicons/icons';
-import { get } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector';
import { GfCreateOrUpdateTagDialogComponent } from './create-or-update-tag-dialog/create-or-update-tag-dialog.component';
@@ -149,7 +148,7 @@ export class GfAdminTagComponent implements OnInit {
this.dataSource = new MatTableDataSource(this.tags);
this.dataSource.sort = this.sort();
- this.dataSource.sortingDataAccessor = get;
+ this.dataSource.sortingDataAccessor = getLowercase;
this.dataService.updateInfo();
From bd7f04be01b26036a577307f7a0268806f1637a2 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Fri, 19 Jun 2026 20:19:20 +0200
Subject: [PATCH 6/7] Task/upgrade yahoo-finance2 to version 3.15.3 (#7067)
* Upgrade yahoo-finance2 to version 3.15.3
* Update changelog
---
CHANGELOG.md | 1 +
package-lock.json | 32 ++++++++++----------------------
package.json | 2 +-
3 files changed, 12 insertions(+), 23 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62c6c83f4..5dc267496 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added the selected asset profile count to the deletion confirmation dialog of the historical market data table in the admin control panel
- Improved the sorting to be case-insensitive in the platform management of the admin control panel
- Improved the sorting to be case-insensitive in the tag management of the admin control panel
+- Upgraded `yahoo-finance2` from version `3.14.2` to `3.15.3`
### Fixed
diff --git a/package-lock.json b/package-lock.json
index 7afc46fd8..0fceee455 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -95,7 +95,7 @@
"tablemark": "4.1.0",
"twitter-api-v2": "1.29.0",
"undici": "7.24.4",
- "yahoo-finance2": "3.14.2",
+ "yahoo-finance2": "3.15.3",
"zone.js": "0.16.1"
},
"devDependencies": {
@@ -4556,7 +4556,6 @@
"version": "1.19.11",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
"integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -7344,7 +7343,6 @@
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@hono/node-server": "^1.19.9",
@@ -18893,7 +18891,6 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -22011,7 +22008,6 @@
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"eventsource-parser": "^3.0.1"
@@ -22155,7 +22151,6 @@
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ip-address": "10.1.0"
@@ -23811,7 +23806,6 @@
"version": "4.12.14",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
"integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -24553,7 +24547,6 @@
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -25222,7 +25215,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "devOptional": true,
"license": "ISC"
},
"node_modules/isobject": {
@@ -28477,7 +28469,6 @@
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
"integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
- "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
@@ -28652,7 +28643,6 @@
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
- "dev": true,
"license": "BSD-2-Clause"
},
"node_modules/json-stable-stringify-without-jsonify": {
@@ -32293,7 +32283,6 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -32531,7 +32520,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
"integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=16.20.0"
@@ -35418,7 +35406,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "devOptional": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -35431,7 +35418,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -39554,7 +39540,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "devOptional": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -39879,19 +39864,23 @@
}
},
"node_modules/yahoo-finance2": {
- "version": "3.14.2",
- "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.14.2.tgz",
- "integrity": "sha512-s+F7TWQT7zAtjhfC7rFHEX16Xfq36u3wceysINP7V+esF3mAYyk9slxZU+fEdkxaTuCT0+PnikHdekMX4UPMrg==",
+ "version": "3.15.3",
+ "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.15.3.tgz",
+ "integrity": "sha512-AFmVZ4ACg3QFT2a/hyJv4Scp2J45gm/6xetVgvUvXzZypqsqbgreXJad4i+qm0onj6yeCf2fPhvow7Eh0CwwzA==",
"license": "MIT",
"dependencies": {
"@deno/shim-deno": "~0.18.0",
+ "@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.26.0",
"fetch-mock-cache": "npm:fetch-mock-cache@^2.1.3",
"json-schema": "^0.4.0",
"tough-cookie": "npm:tough-cookie@^5.1.1",
- "tough-cookie-file-store": "npm:tough-cookie-file-store@^2.0.3"
+ "tough-cookie-file-store": "npm:tough-cookie-file-store@^2.0.3",
+ "zod": "npm:zod@^3.25.0"
},
"bin": {
- "yahoo-finance": "esm/bin/yahoo-finance.js"
+ "yahoo-finance": "esm/bin/yahoo-finance.js",
+ "yahoo-finance-mcp": "esm/bin/yahoo-finance-mcp.js",
+ "yahoo-finance2": "esm/bin/yahoo-finance.js"
},
"engines": {
"node": ">=20.0.0"
@@ -40149,7 +40138,6 @@
"version": "3.25.2",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
"integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
- "dev": true,
"license": "ISC",
"peerDependencies": {
"zod": "^3.25.28 || ^4"
diff --git a/package.json b/package.json
index f6f31747b..6ec921f21 100644
--- a/package.json
+++ b/package.json
@@ -139,7 +139,7 @@
"tablemark": "4.1.0",
"twitter-api-v2": "1.29.0",
"undici": "7.24.4",
- "yahoo-finance2": "3.14.2",
+ "yahoo-finance2": "3.15.3",
"zone.js": "0.16.1"
},
"devDependencies": {
From 5d60887a19241474e85e61a81a9947c9e4589df5 Mon Sep 17 00:00:00 2001
From: Sjohn21
Date: Fri, 19 Jun 2026 20:21:10 +0200
Subject: [PATCH 7/7] Feature/move admin market data endpoint to asset profiles
(#7056)
* Move admin market data endpoint to asset profiles
* Update changelog
---------
Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
---
CHANGELOG.md | 1 +
apps/api/src/app/admin/admin.controller.ts | 38 +-
apps/api/src/app/admin/admin.module.ts | 2 -
apps/api/src/app/admin/admin.service.ts | 386 +----------------
.../asset-profiles.controller.ts | 42 +-
.../asset-profiles/asset-profiles.module.ts | 14 +-
.../asset-profiles/asset-profiles.service.ts | 396 +++++++++++++++++-
.../admin-market-data.component.ts | 18 +-
libs/common/src/lib/helper.ts | 4 +-
...ace.ts => asset-profile-item.interface.ts} | 9 +-
libs/common/src/lib/interfaces/index.ts | 10 +-
.../asset-profiles-response.interface.ts | 6 +
.../src/lib/assistant/assistant.component.ts | 11 +-
libs/ui/src/lib/services/admin.service.ts | 44 +-
libs/ui/src/lib/services/data.service.ts | 37 ++
15 files changed, 517 insertions(+), 501 deletions(-)
rename libs/common/src/lib/interfaces/{admin-market-data.interface.ts => asset-profile-item.interface.ts} (79%)
create mode 100644 libs/common/src/lib/interfaces/responses/asset-profiles-response.interface.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5dc267496..eb67cd23b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- Moved the endpoint to get the asset profiles from `GET api/v1/admin/market-data` to `GET api/v1/asset-profiles`
- Added the selected asset profile count to the delete menu item of the historical market data table in the admin control panel
- Added the selected asset profile count to the deletion confirmation dialog of the historical market data table in the admin control panel
- Improved the sorting to be case-insensitive in the platform management of the admin control panel
diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts
index 1a2c58d1c..9eb045fba 100644
--- a/apps/api/src/app/admin/admin.controller.ts
+++ b/apps/api/src/app/admin/admin.controller.ts
@@ -1,7 +1,6 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
-import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
import { DemoService } from '@ghostfolio/api/services/demo/demo.service';
@@ -24,18 +23,13 @@ import {
} from '@ghostfolio/common/helper';
import {
AdminData,
- AdminMarketData,
AdminUserResponse,
AdminUsersResponse,
EnhancedSymbolProfile,
ScraperConfiguration
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
-import type {
- DateRange,
- MarketDataPreset,
- RequestWithUser
-} from '@ghostfolio/common/types';
+import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
@@ -67,7 +61,6 @@ export class AdminController {
public constructor(
private readonly adminService: AdminService,
- private readonly apiService: ApiService,
private readonly benchmarkService: BenchmarkService,
private readonly dataGatheringService: DataGatheringService,
private readonly demoService: DemoService,
@@ -218,35 +211,6 @@ export class AdminController {
});
}
- @Get('market-data')
- @HasPermission(permissions.accessAdminControl)
- @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
- public async getMarketData(
- @Query('assetSubClasses') filterByAssetSubClasses?: string,
- @Query('dataSource') filterByDataSource?: string,
- @Query('presetId') presetId?: MarketDataPreset,
- @Query('query') filterBySearchQuery?: string,
- @Query('skip') skip?: number,
- @Query('sortColumn') sortColumn?: string,
- @Query('sortDirection') sortDirection?: Prisma.SortOrder,
- @Query('take') take?: number
- ): Promise {
- const filters = this.apiService.buildFiltersFromQueryParams({
- filterByAssetSubClasses,
- filterByDataSource,
- filterBySearchQuery
- });
-
- return this.adminService.getMarketData({
- filters,
- presetId,
- sortColumn,
- sortDirection,
- skip: isNaN(skip) ? undefined : skip,
- take: isNaN(take) ? undefined : take
- });
- }
-
@HasPermission(permissions.accessAdminControl)
@Post('market-data/:dataSource/:symbol/test')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
diff --git a/apps/api/src/app/admin/admin.module.ts b/apps/api/src/app/admin/admin.module.ts
index e87df9e74..0cd4d3c16 100644
--- a/apps/api/src/app/admin/admin.module.ts
+++ b/apps/api/src/app/admin/admin.module.ts
@@ -1,6 +1,5 @@
import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
-import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
@@ -21,7 +20,6 @@ import { QueueModule } from './queue/queue.module';
@Module({
imports: [
ActivitiesModule,
- ApiModule,
BenchmarkModule,
ConfigurationModule,
DataGatheringQueueModule,
diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts
index be6f050c4..46ab25e96 100644
--- a/apps/api/src/app/admin/admin.service.ts
+++ b/apps/api/src/app/admin/admin.service.ts
@@ -1,6 +1,5 @@
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { environment } from '@ghostfolio/api/environments/environment';
-import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
@@ -13,24 +12,15 @@ import {
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_IS_USER_SIGNUP_ENABLED
} from '@ghostfolio/common/config';
-import {
- applyAssetProfileOverrides,
- getAssetProfileIdentifier,
- getCurrencyFromSymbol,
- isCurrency
-} from '@ghostfolio/common/helper';
+import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
import {
AdminData,
- AdminMarketData,
AdminMarketDataDetails,
- AdminMarketDataItem,
AdminUserResponse,
AdminUsersResponse,
AssetProfileIdentifier,
- EnhancedSymbolProfile,
- Filter
+ EnhancedSymbolProfile
} from '@ghostfolio/common/interfaces';
-import { MarketDataPreset } from '@ghostfolio/common/types';
import {
BadRequestException,
@@ -48,13 +38,11 @@ import {
} from '@prisma/client';
import { differenceInDays } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
-import { groupBy } from 'lodash';
@Injectable()
export class AdminService {
public constructor(
private readonly activitiesService: ActivitiesService,
- private readonly benchmarkService: BenchmarkService,
private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
@@ -188,244 +176,6 @@ export class AdminService {
};
}
- public async getMarketData({
- filters,
- presetId,
- sortColumn,
- sortDirection = 'asc',
- skip,
- take = Number.MAX_SAFE_INTEGER
- }: {
- filters?: Filter[];
- presetId?: MarketDataPreset;
- skip?: number;
- sortColumn?: string;
- sortDirection?: Prisma.SortOrder;
- take?: number;
- }): Promise {
- let orderBy: Prisma.Enumerable =
- [{ symbol: 'asc' }];
- const where: Prisma.SymbolProfileWhereInput = {};
-
- if (presetId === 'BENCHMARKS') {
- const benchmarkAssetProfiles =
- await this.benchmarkService.getBenchmarkAssetProfiles();
-
- where.id = {
- in: benchmarkAssetProfiles.map(({ id }) => {
- return id;
- })
- };
- } else if (presetId === 'CURRENCIES') {
- return this.getMarketDataForCurrencies();
- } else if (
- presetId === 'ETF_WITHOUT_COUNTRIES' ||
- presetId === 'ETF_WITHOUT_SECTORS'
- ) {
- filters = [{ id: 'ETF', type: 'ASSET_SUB_CLASS' }];
- } else if (presetId === 'NO_ACTIVITIES') {
- where.activities = {
- none: {}
- };
- }
-
- const searchQuery = filters.find(({ type }) => {
- return type === 'SEARCH_QUERY';
- })?.id;
-
- const {
- ASSET_SUB_CLASS: filtersByAssetSubClass,
- DATA_SOURCE: filtersByDataSource
- } = groupBy(filters, ({ type }) => {
- return type;
- });
-
- const marketDataItems = await this.prismaService.marketData.groupBy({
- _count: true,
- by: ['dataSource', 'symbol']
- });
-
- if (filtersByAssetSubClass) {
- where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id];
- }
-
- if (filtersByDataSource) {
- where.dataSource = DataSource[filtersByDataSource[0].id];
- }
-
- if (searchQuery) {
- where.OR = [
- { id: { mode: 'insensitive', startsWith: searchQuery } },
- { isin: { mode: 'insensitive', startsWith: searchQuery } },
- { name: { mode: 'insensitive', startsWith: searchQuery } },
- { symbol: { mode: 'insensitive', startsWith: searchQuery } }
- ];
- }
-
- if (sortColumn) {
- orderBy = [{ [sortColumn]: sortDirection }];
-
- if (sortColumn === 'activitiesCount') {
- orderBy = [
- {
- activities: {
- _count: sortDirection
- }
- }
- ];
- }
- }
-
- const extendedPrismaClient = this.getExtendedPrismaClient();
-
- const symbolProfileResult = await Promise.all([
- extendedPrismaClient.symbolProfile.findMany({
- skip,
- take,
- where,
- orderBy: [...orderBy, { id: sortDirection }],
- select: {
- _count: {
- select: {
- activities: true,
- watchedBy: true
- }
- },
- activities: {
- orderBy: [{ date: 'asc' }],
- select: { date: true },
- take: 1
- },
- assetClass: true,
- assetSubClass: true,
- comment: true,
- countries: true,
- currency: true,
- dataSource: true,
- id: true,
- isActive: true,
- isUsedByUsersWithSubscription: true,
- name: true,
- scraperConfiguration: true,
- sectors: true,
- symbol: true,
- SymbolProfileOverrides: true
- }
- }),
- this.prismaService.symbolProfile.count({ where })
- ]);
- const assetProfiles = symbolProfileResult[0];
- let count = symbolProfileResult[1];
-
- const lastMarketPrices = await this.prismaService.marketData.findMany({
- distinct: ['dataSource', 'symbol'],
- orderBy: { date: 'desc' },
- select: {
- dataSource: true,
- marketPrice: true,
- symbol: true
- },
- where: {
- dataSource: {
- in: assetProfiles.map(({ dataSource }) => {
- return dataSource;
- })
- },
- symbol: {
- in: assetProfiles.map(({ symbol }) => {
- return symbol;
- })
- }
- }
- });
-
- const lastMarketPriceMap = new Map();
-
- for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
- lastMarketPriceMap.set(
- getAssetProfileIdentifier({ dataSource, symbol }),
- marketPrice
- );
- }
-
- let marketData: AdminMarketDataItem[] = await Promise.all(
- assetProfiles.map(async (assetProfile) => {
- const {
- _count,
- activities,
- comment,
- currency,
- dataSource,
- id,
- isActive,
- isUsedByUsersWithSubscription,
- symbol
- } = assetProfile;
-
- const { assetClass, assetSubClass, countries, name, sectors } =
- applyAssetProfileOverrides(
- assetProfile,
- assetProfile.SymbolProfileOverrides
- );
-
- const countriesCount = countries ? Object.keys(countries).length : 0;
-
- const lastMarketPrice = lastMarketPriceMap.get(
- getAssetProfileIdentifier({ dataSource, symbol })
- );
-
- const marketDataItemCount =
- marketDataItems.find((marketDataItem) => {
- return (
- marketDataItem.dataSource === dataSource &&
- marketDataItem.symbol === symbol
- );
- })?._count ?? 0;
-
- const sectorsCount = sectors ? Object.keys(sectors).length : 0;
-
- return {
- assetClass,
- assetSubClass,
- comment,
- countriesCount,
- currency,
- dataSource,
- id,
- isActive,
- lastMarketPrice,
- marketDataItemCount,
- name,
- sectorsCount,
- symbol,
- activitiesCount: _count.activities,
- date: activities?.[0]?.date,
- isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription,
- watchedByCount: _count.watchedBy
- };
- })
- );
-
- if (presetId) {
- if (presetId === 'ETF_WITHOUT_COUNTRIES') {
- marketData = marketData.filter(({ countriesCount }) => {
- return countriesCount === 0;
- });
- } else if (presetId === 'ETF_WITHOUT_SECTORS') {
- marketData = marketData.filter(({ sectorsCount }) => {
- return sectorsCount === 0;
- });
- }
-
- count = marketData.length;
- }
-
- return {
- count,
- marketData
- };
- }
-
public async getMarketDataBySymbol({
dataSource,
symbol
@@ -667,138 +417,6 @@ export class AdminService {
});
}
- private getExtendedPrismaClient() {
- const symbolProfileExtension = Prisma.defineExtension((client) => {
- return client.$extends({
- result: {
- symbolProfile: {
- isUsedByUsersWithSubscription: {
- compute: async ({ id }) => {
- const { _count } =
- await this.prismaService.symbolProfile.findUnique({
- select: {
- _count: {
- select: {
- activities: {
- where: {
- user: {
- subscriptions: {
- some: {
- expiresAt: {
- gt: new Date()
- }
- }
- }
- }
- }
- }
- }
- }
- },
- where: {
- id
- }
- });
-
- return _count.activities > 0;
- }
- }
- }
- }
- });
- });
-
- return this.prismaService.$extends(symbolProfileExtension);
- }
-
- private async getMarketDataForCurrencies(): Promise {
- const currencyPairs = this.exchangeRateDataService.getCurrencyPairs();
-
- const [lastMarketPrices, marketDataItems] = await Promise.all([
- this.prismaService.marketData.findMany({
- distinct: ['dataSource', 'symbol'],
- orderBy: { date: 'desc' },
- select: {
- dataSource: true,
- marketPrice: true,
- symbol: true
- },
- where: {
- dataSource: {
- in: currencyPairs.map(({ dataSource }) => {
- return dataSource;
- })
- },
- symbol: {
- in: currencyPairs.map(({ symbol }) => {
- return symbol;
- })
- }
- }
- }),
- this.prismaService.marketData.groupBy({
- _count: true,
- by: ['dataSource', 'symbol']
- })
- ]);
-
- const lastMarketPriceMap = new Map();
-
- for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
- lastMarketPriceMap.set(
- getAssetProfileIdentifier({ dataSource, symbol }),
- marketPrice
- );
- }
-
- const marketDataPromise: Promise[] = currencyPairs.map(
- async ({ dataSource, symbol }) => {
- let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
- let currency: EnhancedSymbolProfile['currency'] = '-';
- let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
-
- if (isCurrency(getCurrencyFromSymbol(symbol))) {
- currency = getCurrencyFromSymbol(symbol);
- ({ activitiesCount, dateOfFirstActivity } =
- await this.activitiesService.getStatisticsByCurrency(currency));
- }
-
- const lastMarketPrice = lastMarketPriceMap.get(
- getAssetProfileIdentifier({ dataSource, symbol })
- );
-
- const marketDataItemCount =
- marketDataItems.find((marketDataItem) => {
- return (
- marketDataItem.dataSource === dataSource &&
- marketDataItem.symbol === symbol
- );
- })?._count ?? 0;
-
- return {
- activitiesCount,
- currency,
- dataSource,
- lastMarketPrice,
- marketDataItemCount,
- symbol,
- assetClass: AssetClass.LIQUIDITY,
- assetSubClass: AssetSubClass.CASH,
- countriesCount: 0,
- date: dateOfFirstActivity,
- id: undefined,
- isActive: true,
- name: symbol,
- sectorsCount: 0,
- watchedByCount: 0
- };
- }
- );
-
- const marketData = await Promise.all(marketDataPromise);
- return { marketData, count: marketData.length };
- }
-
private async getUsersWithAnalytics({
skip,
take,
diff --git a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts
index 38227c555..2606d8075 100644
--- a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts
+++ b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts
@@ -1,22 +1,28 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
+import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos';
-import { EnhancedSymbolProfile } from '@ghostfolio/common/interfaces';
+import {
+ AssetProfilesResponse,
+ EnhancedSymbolProfile
+} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
-import { RequestWithUser } from '@ghostfolio/common/types';
+import { MarketDataPreset, RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
Controller,
+ Get,
HttpException,
Inject,
Param,
Patch,
+ Query,
UseGuards
} from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
-import { DataSource } from '@prisma/client';
+import { DataSource, Prisma } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { AssetProfilesService } from './asset-profiles.service';
@@ -24,10 +30,40 @@ import { AssetProfilesService } from './asset-profiles.service';
@Controller('asset-profiles')
export class AssetProfilesController {
public constructor(
+ private readonly apiService: ApiService,
private readonly assetProfilesService: AssetProfilesService,
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
+ @Get()
+ @HasPermission(permissions.accessAdminControl)
+ @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
+ public async getAssetProfiles(
+ @Query('assetSubClasses') filterByAssetSubClasses?: string,
+ @Query('dataSource') filterByDataSource?: string,
+ @Query('presetId') presetId?: MarketDataPreset,
+ @Query('query') filterBySearchQuery?: string,
+ @Query('skip') skip?: number,
+ @Query('sortColumn') sortColumn?: string,
+ @Query('sortDirection') sortDirection?: Prisma.SortOrder,
+ @Query('take') take?: number
+ ): Promise {
+ const filters = this.apiService.buildFiltersFromQueryParams({
+ filterByAssetSubClasses,
+ filterByDataSource,
+ filterBySearchQuery
+ });
+
+ return this.assetProfilesService.getAssetProfiles({
+ filters,
+ presetId,
+ sortColumn,
+ sortDirection,
+ skip: isNaN(skip) ? undefined : skip,
+ take: isNaN(take) ? undefined : take
+ });
+ }
+
@HasPermission(permissions.accessAdminControl)
@Patch(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
diff --git a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts
index 32b9ab393..98463ce5d 100644
--- a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts
+++ b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts
@@ -1,3 +1,8 @@
+import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module';
+import { ApiModule } from '@ghostfolio/api/services/api/api.module';
+import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
+import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
+import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common';
@@ -7,7 +12,14 @@ import { AssetProfilesService } from './asset-profiles.service';
@Module({
controllers: [AssetProfilesController],
- imports: [SymbolProfileModule],
+ imports: [
+ ActivitiesModule,
+ ApiModule,
+ BenchmarkModule,
+ ExchangeRateDataModule,
+ PrismaModule,
+ SymbolProfileModule
+ ],
providers: [AssetProfilesService]
})
export class AssetProfilesModule {}
diff --git a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts
index ef24372af..39eaa1642 100644
--- a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts
+++ b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts
@@ -1,19 +1,279 @@
+import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
+import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
+import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
+import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos';
import {
+ applyAssetProfileOverrides,
+ getAssetProfileIdentifier,
+ getCurrencyFromSymbol,
+ isCurrency
+} from '@ghostfolio/common/helper';
+import {
+ AssetProfileItem,
AssetProfileIdentifier,
- EnhancedSymbolProfile
+ AssetProfilesResponse,
+ EnhancedSymbolProfile,
+ Filter
} from '@ghostfolio/common/interfaces';
+import { MarketDataPreset } from '@ghostfolio/common/types';
import { Injectable, NotFoundException } from '@nestjs/common';
-import { Prisma } from '@prisma/client';
+import { AssetClass, AssetSubClass, DataSource, Prisma } from '@prisma/client';
+import { groupBy } from 'lodash';
@Injectable()
export class AssetProfilesService {
public constructor(
+ private readonly activitiesService: ActivitiesService,
+ private readonly benchmarkService: BenchmarkService,
+ private readonly exchangeRateDataService: ExchangeRateDataService,
+ private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService
) {}
+ public async getAssetProfiles({
+ filters = [],
+ presetId,
+ sortColumn,
+ sortDirection = 'asc',
+ skip,
+ take = Number.MAX_SAFE_INTEGER
+ }: {
+ filters?: Filter[];
+ presetId?: MarketDataPreset;
+ skip?: number;
+ sortColumn?: string;
+ sortDirection?: Prisma.SortOrder;
+ take?: number;
+ }): Promise {
+ let orderBy: Prisma.Enumerable =
+ [{ symbol: 'asc' }];
+ const where: Prisma.SymbolProfileWhereInput = {};
+
+ if (presetId === 'BENCHMARKS') {
+ const benchmarkAssetProfiles =
+ await this.benchmarkService.getBenchmarkAssetProfiles();
+
+ where.id = {
+ in: benchmarkAssetProfiles.map(({ id }) => {
+ return id;
+ })
+ };
+ } else if (presetId === 'CURRENCIES') {
+ return this.getAssetProfilesForCurrencies();
+ } else if (
+ presetId === 'ETF_WITHOUT_COUNTRIES' ||
+ presetId === 'ETF_WITHOUT_SECTORS'
+ ) {
+ filters = [{ id: 'ETF', type: 'ASSET_SUB_CLASS' }];
+ } else if (presetId === 'NO_ACTIVITIES') {
+ where.activities = {
+ none: {}
+ };
+ }
+
+ const searchQuery = filters.find(({ type }) => {
+ return type === 'SEARCH_QUERY';
+ })?.id;
+
+ const {
+ ASSET_SUB_CLASS: filtersByAssetSubClass,
+ DATA_SOURCE: filtersByDataSource
+ } = groupBy(filters, ({ type }) => {
+ return type;
+ });
+
+ const marketDataItems = await this.prismaService.marketData.groupBy({
+ _count: true,
+ by: ['dataSource', 'symbol']
+ });
+
+ if (filtersByAssetSubClass) {
+ where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id];
+ }
+
+ if (filtersByDataSource) {
+ where.dataSource = DataSource[filtersByDataSource[0].id];
+ }
+
+ if (searchQuery) {
+ where.OR = [
+ { id: { mode: 'insensitive', startsWith: searchQuery } },
+ { isin: { mode: 'insensitive', startsWith: searchQuery } },
+ { name: { mode: 'insensitive', startsWith: searchQuery } },
+ { symbol: { mode: 'insensitive', startsWith: searchQuery } }
+ ];
+ }
+
+ if (sortColumn) {
+ orderBy = [{ [sortColumn]: sortDirection }];
+
+ if (sortColumn === 'activitiesCount') {
+ orderBy = [
+ {
+ activities: {
+ _count: sortDirection
+ }
+ }
+ ];
+ }
+ }
+
+ const extendedPrismaClient = this.getExtendedPrismaClient();
+
+ const symbolProfileResult = await Promise.all([
+ extendedPrismaClient.symbolProfile.findMany({
+ skip,
+ take,
+ where,
+ orderBy: [...orderBy, { id: sortDirection }],
+ select: {
+ _count: {
+ select: {
+ activities: true,
+ watchedBy: true
+ }
+ },
+ activities: {
+ orderBy: [{ date: 'asc' }],
+ select: { date: true },
+ take: 1
+ },
+ assetClass: true,
+ assetSubClass: true,
+ comment: true,
+ countries: true,
+ currency: true,
+ dataSource: true,
+ id: true,
+ isin: true,
+ isActive: true,
+ isUsedByUsersWithSubscription: true,
+ name: true,
+ scraperConfiguration: true,
+ sectors: true,
+ symbol: true,
+ SymbolProfileOverrides: true
+ }
+ }),
+ this.prismaService.symbolProfile.count({ where })
+ ]);
+ const symbolProfiles = symbolProfileResult[0];
+ let count = symbolProfileResult[1];
+
+ const lastMarketPrices = await this.prismaService.marketData.findMany({
+ distinct: ['dataSource', 'symbol'],
+ orderBy: { date: 'desc' },
+ select: {
+ dataSource: true,
+ marketPrice: true,
+ symbol: true
+ },
+ where: {
+ dataSource: {
+ in: symbolProfiles.map(({ dataSource }) => {
+ return dataSource;
+ })
+ },
+ symbol: {
+ in: symbolProfiles.map(({ symbol }) => {
+ return symbol;
+ })
+ }
+ }
+ });
+
+ const lastMarketPriceMap = new Map();
+
+ for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
+ lastMarketPriceMap.set(
+ getAssetProfileIdentifier({ dataSource, symbol }),
+ marketPrice
+ );
+ }
+
+ let assetProfiles: AssetProfileItem[] = await Promise.all(
+ symbolProfiles.map(async (assetProfile) => {
+ const {
+ _count,
+ activities,
+ comment,
+ currency,
+ dataSource,
+ id,
+ isin,
+ isActive,
+ isUsedByUsersWithSubscription,
+ symbol
+ } = assetProfile;
+
+ const { assetClass, assetSubClass, countries, name, sectors } =
+ applyAssetProfileOverrides(
+ assetProfile,
+ assetProfile.SymbolProfileOverrides
+ );
+
+ const countriesCount = countries ? Object.keys(countries).length : 0;
+
+ const lastMarketPrice = lastMarketPriceMap.get(
+ getAssetProfileIdentifier({ dataSource, symbol })
+ );
+
+ const marketDataItemCount =
+ marketDataItems.find((marketDataItem) => {
+ return (
+ marketDataItem.dataSource === dataSource &&
+ marketDataItem.symbol === symbol
+ );
+ })?._count ?? 0;
+
+ const sectorsCount = sectors ? Object.keys(sectors).length : 0;
+
+ return {
+ assetClass,
+ assetSubClass,
+ comment,
+ countriesCount,
+ currency,
+ dataSource,
+ id,
+ isActive,
+ isin,
+ lastMarketPrice,
+ marketDataItemCount,
+ name,
+ sectorsCount,
+ symbol,
+ activitiesCount: _count.activities,
+ date: activities?.[0]?.date,
+ isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription,
+ watchedByCount: _count.watchedBy
+ };
+ })
+ );
+
+ if (presetId) {
+ if (presetId === 'ETF_WITHOUT_COUNTRIES') {
+ assetProfiles = assetProfiles.filter(({ countriesCount }) => {
+ return countriesCount === 0;
+ });
+ } else if (presetId === 'ETF_WITHOUT_SECTORS') {
+ assetProfiles = assetProfiles.filter(({ sectorsCount }) => {
+ return sectorsCount === 0;
+ });
+ }
+
+ count = assetProfiles.length;
+ }
+
+ return {
+ assetProfiles,
+ count
+ };
+ }
+
public async updateAssetProfileData(
{ dataSource, symbol }: AssetProfileIdentifier,
assetProfileData: UpdateAssetProfileDataDto
@@ -87,4 +347,136 @@ export class AssetProfilesService {
return data;
}
+
+ private async getAssetProfilesForCurrencies(): Promise {
+ const currencyPairs = this.exchangeRateDataService.getCurrencyPairs();
+
+ const [lastMarketPrices, marketDataItems] = await Promise.all([
+ this.prismaService.marketData.findMany({
+ distinct: ['dataSource', 'symbol'],
+ orderBy: { date: 'desc' },
+ select: {
+ dataSource: true,
+ marketPrice: true,
+ symbol: true
+ },
+ where: {
+ dataSource: {
+ in: currencyPairs.map(({ dataSource }) => {
+ return dataSource;
+ })
+ },
+ symbol: {
+ in: currencyPairs.map(({ symbol }) => {
+ return symbol;
+ })
+ }
+ }
+ }),
+ this.prismaService.marketData.groupBy({
+ _count: true,
+ by: ['dataSource', 'symbol']
+ })
+ ]);
+
+ const lastMarketPriceMap = new Map();
+
+ for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
+ lastMarketPriceMap.set(
+ getAssetProfileIdentifier({ dataSource, symbol }),
+ marketPrice
+ );
+ }
+
+ const assetProfilePromises: Promise[] = currencyPairs.map(
+ async ({ dataSource, symbol }) => {
+ let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
+ let currency: EnhancedSymbolProfile['currency'] = '-';
+ let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
+
+ if (isCurrency(getCurrencyFromSymbol(symbol))) {
+ currency = getCurrencyFromSymbol(symbol);
+ ({ activitiesCount, dateOfFirstActivity } =
+ await this.activitiesService.getStatisticsByCurrency(currency));
+ }
+
+ const lastMarketPrice = lastMarketPriceMap.get(
+ getAssetProfileIdentifier({ dataSource, symbol })
+ );
+
+ const marketDataItemCount =
+ marketDataItems.find((marketDataItem) => {
+ return (
+ marketDataItem.dataSource === dataSource &&
+ marketDataItem.symbol === symbol
+ );
+ })?._count ?? 0;
+
+ return {
+ activitiesCount,
+ currency,
+ dataSource,
+ lastMarketPrice,
+ marketDataItemCount,
+ symbol,
+ assetClass: AssetClass.LIQUIDITY,
+ assetSubClass: AssetSubClass.CASH,
+ countriesCount: 0,
+ date: dateOfFirstActivity,
+ id: undefined,
+ isActive: true,
+ name: symbol,
+ sectorsCount: 0,
+ watchedByCount: 0
+ };
+ }
+ );
+
+ const assetProfiles = await Promise.all(assetProfilePromises);
+ return { assetProfiles, count: assetProfiles.length };
+ }
+
+ private getExtendedPrismaClient() {
+ const symbolProfileExtension = Prisma.defineExtension((client) => {
+ return client.$extends({
+ result: {
+ symbolProfile: {
+ isUsedByUsersWithSubscription: {
+ compute: async ({ id }) => {
+ const { _count } =
+ await this.prismaService.symbolProfile.findUnique({
+ select: {
+ _count: {
+ select: {
+ activities: {
+ where: {
+ user: {
+ subscriptions: {
+ some: {
+ expiresAt: {
+ gt: new Date()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ where: {
+ id
+ }
+ });
+
+ return _count.activities > 0;
+ }
+ }
+ }
+ }
+ });
+ });
+
+ return this.prismaService.$extends(symbolProfileExtension);
+ }
}
diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
index a4a9b6e19..f7396eb1d 100644
--- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
+++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
@@ -10,11 +10,11 @@ import {
} from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
+ AssetProfileItem,
Filter,
InfoItem,
User
} from '@ghostfolio/common/interfaces';
-import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfSymbolPipe } from '@ghostfolio/common/pipes';
import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter';
@@ -152,7 +152,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit {
}
];
protected readonly canDeleteAssetProfile = canDeleteAssetProfile;
- protected dataSource = new MatTableDataSource();
+ protected dataSource = new MatTableDataSource();
protected defaultDateFormat: string;
protected readonly displayedColumns: string[] = [];
protected readonly filters$ = new Subject();
@@ -160,7 +160,7 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit {
protected readonly isUUID = isUUID;
protected pageSize = DEFAULT_PAGE_SIZE;
protected placeholder = '';
- protected readonly selection = new SelectionModel(true);
+ protected readonly selection = new SelectionModel(true);
protected totalItems = 0;
protected user: User;
@@ -375,8 +375,8 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit {
this.selection.clear();
- this.adminService
- .fetchAdminMarketData({
+ this.dataService
+ .fetchAssetProfiles({
sortColumn,
sortDirection,
filters: this.activeFilters,
@@ -384,15 +384,15 @@ export class GfAdminMarketDataComponent implements AfterViewInit, OnInit {
take: this.pageSize
})
.pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe(({ count, marketData }) => {
+ .subscribe(({ assetProfiles, count }) => {
this.totalItems = count;
this.dataSource = new MatTableDataSource(
- marketData.map((marketDataItem) => {
+ assetProfiles.map((assetProfile) => {
return {
- ...marketDataItem,
+ ...assetProfile,
isBenchmark: this.benchmarks.some(({ id }) => {
- return id === marketDataItem.id;
+ return id === assetProfile.id;
})
};
})
diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts
index 3ca95d9f6..5bd6671f2 100644
--- a/libs/common/src/lib/helper.ts
+++ b/libs/common/src/lib/helper.ts
@@ -44,8 +44,8 @@ import {
ghostfolioScraperApiSymbolPrefix
} from './config';
import {
- AdminMarketDataItem,
AssetProfileIdentifier,
+ AssetProfileItem,
Benchmark
} from './interfaces';
import { BenchmarkTrend, ColorScheme } from './types';
@@ -149,7 +149,7 @@ export function canDeleteAssetProfile({
symbol,
watchedByCount
}: Pick<
- AdminMarketDataItem,
+ AssetProfileItem,
'activitiesCount' | 'isBenchmark' | 'symbol' | 'watchedByCount'
>): boolean {
return (
diff --git a/libs/common/src/lib/interfaces/admin-market-data.interface.ts b/libs/common/src/lib/interfaces/asset-profile-item.interface.ts
similarity index 79%
rename from libs/common/src/lib/interfaces/admin-market-data.interface.ts
rename to libs/common/src/lib/interfaces/asset-profile-item.interface.ts
index 953f94e26..14e8471c1 100644
--- a/libs/common/src/lib/interfaces/admin-market-data.interface.ts
+++ b/libs/common/src/lib/interfaces/asset-profile-item.interface.ts
@@ -1,14 +1,10 @@
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
-export interface AdminMarketData {
- count: number;
- marketData: AdminMarketDataItem[];
-}
-
-export interface AdminMarketDataItem {
+export interface AssetProfileItem {
activitiesCount: number;
assetClass?: AssetClass;
assetSubClass?: AssetSubClass;
+ comment?: string;
countriesCount: number;
currency: string;
dataSource: DataSource;
@@ -16,6 +12,7 @@ export interface AdminMarketDataItem {
id: string;
isActive: boolean;
isBenchmark?: boolean;
+ isin?: string;
isUsedByUsersWithSubscription?: boolean;
lastMarketPrice: number;
marketDataItemCount: number;
diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts
index 6c076514c..989decdba 100644
--- a/libs/common/src/lib/interfaces/index.ts
+++ b/libs/common/src/lib/interfaces/index.ts
@@ -4,13 +4,10 @@ import type { Activity, ActivityError } from './activities.interface';
import type { AdminData } from './admin-data.interface';
import type { AdminJobs } from './admin-jobs.interface';
import type { AdminMarketDataDetails } from './admin-market-data-details.interface';
-import type {
- AdminMarketData,
- AdminMarketDataItem
-} from './admin-market-data.interface';
import type { AdminUser } from './admin-user.interface';
import type { AssetClassSelectorOption } from './asset-class-selector-option.interface';
import type { AssetProfileIdentifier } from './asset-profile-identifier.interface';
+import type { AssetProfileItem } from './asset-profile-item.interface';
import type { BenchmarkProperty } from './benchmark-property.interface';
import type { Benchmark } from './benchmark.interface';
import type { Coupon } from './coupon.interface';
@@ -50,6 +47,7 @@ import type { AdminUsersResponse } from './responses/admin-users-response.interf
import type { AiPromptResponse } from './responses/ai-prompt-response.interface';
import type { AiServiceHealthResponse } from './responses/ai-service-health-response.interface';
import type { ApiKeyResponse } from './responses/api-key-response.interface';
+import type { AssetProfilesResponse } from './responses/asset-profiles-response.interface';
import type { AssetResponse } from './responses/asset-response.interface';
import type { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-response.interface';
import type { BenchmarkResponse } from './responses/benchmark-response.interface';
@@ -114,9 +112,7 @@ export {
ActivityResponse,
AdminData,
AdminJobs,
- AdminMarketData,
AdminMarketDataDetails,
- AdminMarketDataItem,
AdminUser,
AdminUserResponse,
AdminUsersResponse,
@@ -126,6 +122,8 @@ export {
AssertionCredentialJSON,
AssetClassSelectorOption,
AssetProfileIdentifier,
+ AssetProfileItem,
+ AssetProfilesResponse,
AssetResponse,
AttestationCredentialJSON,
Benchmark,
diff --git a/libs/common/src/lib/interfaces/responses/asset-profiles-response.interface.ts b/libs/common/src/lib/interfaces/responses/asset-profiles-response.interface.ts
new file mode 100644
index 000000000..e73e4c64f
--- /dev/null
+++ b/libs/common/src/lib/interfaces/responses/asset-profiles-response.interface.ts
@@ -0,0 +1,6 @@
+import { AssetProfileItem } from '../asset-profile-item.interface';
+
+export interface AssetProfilesResponse {
+ assetProfiles: AssetProfileItem[];
+ count: number;
+}
diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts
index e52cefcbc..3f8efa6ec 100644
--- a/libs/ui/src/lib/assistant/assistant.component.ts
+++ b/libs/ui/src/lib/assistant/assistant.component.ts
@@ -3,7 +3,7 @@ import { Filter, PortfolioPosition, User } from '@ghostfolio/common/interfaces';
import { InternalRoute } from '@ghostfolio/common/routes/interfaces/internal-route.interface';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { AccountWithPlatform, DateRange } from '@ghostfolio/common/types';
-import { AdminService, DataService } from '@ghostfolio/ui/services';
+import { DataService } from '@ghostfolio/ui/services';
import { FocusKeyManager } from '@angular/cdk/a11y';
import {
@@ -155,7 +155,6 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
private preselectionTimeout: ReturnType;
public constructor(
- private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private destroyRef: DestroyRef
@@ -674,8 +673,8 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
private searchAssetProfiles(
aSearchTerm: string
): Observable {
- return this.adminService
- .fetchAdminMarketData({
+ return this.dataService
+ .fetchAssetProfiles({
filters: [
{
id: aSearchTerm,
@@ -688,8 +687,8 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
catchError(() => {
return EMPTY;
}),
- map(({ marketData }) => {
- return marketData.map(
+ map(({ assetProfiles }) => {
+ return assetProfiles.map(
({ assetSubClass, currency, dataSource, name, symbol }) => {
return {
currency,
diff --git a/libs/ui/src/lib/services/admin.service.ts b/libs/ui/src/lib/services/admin.service.ts
index 094001c2f..0eea768d6 100644
--- a/libs/ui/src/lib/services/admin.service.ts
+++ b/libs/ui/src/lib/services/admin.service.ts
@@ -11,32 +11,26 @@ import {
import {
AdminData,
AdminJobs,
- AdminMarketData,
AdminUserResponse,
AdminUsersResponse,
AssetProfileIdentifier,
DataProviderGhostfolioStatusResponse,
DataProviderHistoricalResponse,
- EnhancedSymbolProfile,
- Filter
+ EnhancedSymbolProfile
} from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types';
import { GF_ENVIRONMENT } from '@ghostfolio/ui/environment';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
-import { SortDirection } from '@angular/material/sort';
import { DataSource, MarketData, Platform } from '@prisma/client';
import { JobStatus } from 'bull';
import { isNumber } from 'lodash';
-import { DataService } from './data.service';
-
@Injectable({
providedIn: 'root'
})
export class AdminService {
- private readonly dataService = inject(DataService);
private readonly environment = inject(GF_ENVIRONMENT);
private readonly http = inject(HttpClient);
@@ -81,42 +75,6 @@ export class AdminService {
return this.http.get('/api/v1/admin');
}
- public fetchAdminMarketData({
- filters,
- skip,
- sortColumn,
- sortDirection,
- take
- }: {
- filters?: Filter[];
- skip?: number;
- sortColumn?: string;
- sortDirection?: SortDirection;
- take: number;
- }) {
- let params = this.dataService.buildFiltersAsQueryParams({ filters });
-
- if (skip) {
- params = params.append('skip', skip);
- }
-
- if (sortColumn) {
- params = params.append('sortColumn', sortColumn);
- }
-
- if (sortDirection) {
- params = params.append('sortDirection', sortDirection);
- }
-
- if (take) {
- params = params.append('take', take);
- }
-
- return this.http.get('/api/v1/admin/market-data', {
- params
- });
- }
-
public fetchGhostfolioDataProviderStatus(aApiKey: string) {
const headers = new HttpHeaders({
[HEADER_KEY_SKIP_INTERCEPTOR]: 'true',
diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts
index a3d8bec98..079ea1bf6 100644
--- a/libs/ui/src/lib/services/data.service.ts
+++ b/libs/ui/src/lib/services/data.service.ts
@@ -28,6 +28,7 @@ import {
AiPromptResponse,
ApiKeyResponse,
AssetProfileIdentifier,
+ AssetProfilesResponse,
AssetResponse,
BenchmarkMarketDataDetailsResponse,
BenchmarkResponse,
@@ -378,6 +379,42 @@ export class DataService {
);
}
+ public fetchAssetProfiles({
+ filters,
+ skip,
+ sortColumn,
+ sortDirection,
+ take
+ }: {
+ filters?: Filter[];
+ skip?: number;
+ sortColumn?: string;
+ sortDirection?: SortDirection;
+ take: number;
+ }) {
+ let params = this.buildFiltersAsQueryParams({ filters });
+
+ if (skip) {
+ params = params.append('skip', skip);
+ }
+
+ if (sortColumn) {
+ params = params.append('sortColumn', sortColumn);
+ }
+
+ if (sortDirection) {
+ params = params.append('sortDirection', sortDirection);
+ }
+
+ if (take) {
+ params = params.append('take', take);
+ }
+
+ return this.http.get('/api/v1/asset-profiles', {
+ params
+ });
+ }
+
public fetchBenchmarkForUser({
dataSource,
filters,