diff --git a/CHANGELOG.md b/CHANGELOG.md index b69e9703f..a5dc0dc3c 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 ### Added +- Added the current market price column to the historical market data table of the admin control - Introduced filters (`dataSource` and `symbol`) in the accounts endpoint ### Changed diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 143d5c1ca..6c3d7c141 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -15,7 +15,11 @@ import { PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_USER_SIGNUP_ENABLED } from '@ghostfolio/common/config'; -import { isCurrency, getCurrencyFromSymbol } from '@ghostfolio/common/helper'; +import { + getAssetProfileIdentifier, + getCurrencyFromSymbol, + isCurrency +} from '@ghostfolio/common/helper'; import { AdminData, AdminMarketData, @@ -261,6 +265,37 @@ export class AdminService { this.prismaService.symbolProfile.count({ where }) ]); + 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 ({ @@ -281,6 +316,11 @@ export class AdminService { const countriesCount = countries ? Object.keys(countries).length : 0; + + const lastMarketPrice = lastMarketPriceMap.get( + getAssetProfileIdentifier({ dataSource, symbol }) + ); + const marketDataItemCount = marketDataItems.find((marketDataItem) => { return ( @@ -288,6 +328,7 @@ export class AdminService { marketDataItem.symbol === symbol ); })?._count ?? 0; + const sectorsCount = sectors ? Object.keys(sectors).length : 0; return { @@ -298,6 +339,7 @@ export class AdminService { countriesCount, dataSource, id, + lastMarketPrice, name, symbol, marketDataItemCount, @@ -511,48 +553,86 @@ export class AdminService { } private async getMarketDataForCurrencies(): Promise { - const marketDataItems = await this.prismaService.marketData.groupBy({ - _count: true, - by: ['dataSource', 'symbol'] - }); - - const marketDataPromise: Promise[] = - this.exchangeRateDataService - .getCurrencyPairs() - .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.orderService.getStatisticsByCurrency(currency)); + 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 marketDataItemCount = - marketDataItems.find((marketDataItem) => { - return ( - marketDataItem.dataSource === dataSource && - marketDataItem.symbol === symbol - ); - })?._count ?? 0; + const lastMarketPriceMap = new Map(); - return { - activitiesCount, - currency, - dataSource, - marketDataItemCount, - symbol, - assetClass: AssetClass.LIQUIDITY, - assetSubClass: AssetSubClass.CASH, - countriesCount: 0, - date: dateOfFirstActivity, - id: undefined, - name: symbol, - sectorsCount: 0 - }; - }); + 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.orderService.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, + name: symbol, + sectorsCount: 0 + }; + } + ); const marketData = await Promise.all(marketDataPromise); return { marketData, count: marketData.length }; 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 98a1d0480..549708d87 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 @@ -142,6 +142,7 @@ export class AdminMarketDataComponent 'dataSource', 'assetClass', 'assetSubClass', + 'lastMarketPrice', 'date', 'activitiesCount', 'marketDataItemCount', 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 f3b2d8ddd..a151d1cd0 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 @@ -99,6 +99,21 @@ + + + Market Price + + +
+ +
+ +
+ First Activity diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts index 224e3506b..161847f99 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.module.ts @@ -1,6 +1,7 @@ import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; +import { GfValueComponent } from '@ghostfolio/ui/value'; import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; @@ -27,6 +28,7 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/ GfCreateAssetProfileDialogModule, GfPremiumIndicatorComponent, GfSymbolModule, + GfValueComponent, MatButtonModule, MatCheckboxModule, MatMenuModule, diff --git a/libs/common/src/lib/interfaces/admin-market-data.interface.ts b/libs/common/src/lib/interfaces/admin-market-data.interface.ts index 420bde826..90fceadb3 100644 --- a/libs/common/src/lib/interfaces/admin-market-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-market-data.interface.ts @@ -16,6 +16,7 @@ export interface AdminMarketDataItem { id: string; isBenchmark?: boolean; isUsedByUsersWithSubscription?: boolean; + lastMarketPrice: number; marketDataItemCount: number; name: string; sectorsCount: number;