Feature/extend market data endpoint by lastMarketPrice (#3752)
* Extend market data endpoint by lastMarketPrice * Integrate last market price in admin market data * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
This commit is contained in:
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### 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
|
- Introduced filters (`dataSource` and `symbol`) in the accounts endpoint
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@@ -15,7 +15,11 @@ import {
|
|||||||
PROPERTY_IS_READ_ONLY_MODE,
|
PROPERTY_IS_READ_ONLY_MODE,
|
||||||
PROPERTY_IS_USER_SIGNUP_ENABLED
|
PROPERTY_IS_USER_SIGNUP_ENABLED
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
import { isCurrency, getCurrencyFromSymbol } from '@ghostfolio/common/helper';
|
import {
|
||||||
|
getAssetProfileIdentifier,
|
||||||
|
getCurrencyFromSymbol,
|
||||||
|
isCurrency
|
||||||
|
} from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
@@ -261,6 +265,37 @@ export class AdminService {
|
|||||||
this.prismaService.symbolProfile.count({ where })
|
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<string, number>();
|
||||||
|
|
||||||
|
for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
|
||||||
|
lastMarketPriceMap.set(
|
||||||
|
getAssetProfileIdentifier({ dataSource, symbol }),
|
||||||
|
marketPrice
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let marketData: AdminMarketDataItem[] = await Promise.all(
|
let marketData: AdminMarketDataItem[] = await Promise.all(
|
||||||
assetProfiles.map(
|
assetProfiles.map(
|
||||||
async ({
|
async ({
|
||||||
@@ -281,6 +316,11 @@ export class AdminService {
|
|||||||
const countriesCount = countries
|
const countriesCount = countries
|
||||||
? Object.keys(countries).length
|
? Object.keys(countries).length
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
|
const lastMarketPrice = lastMarketPriceMap.get(
|
||||||
|
getAssetProfileIdentifier({ dataSource, symbol })
|
||||||
|
);
|
||||||
|
|
||||||
const marketDataItemCount =
|
const marketDataItemCount =
|
||||||
marketDataItems.find((marketDataItem) => {
|
marketDataItems.find((marketDataItem) => {
|
||||||
return (
|
return (
|
||||||
@@ -288,6 +328,7 @@ export class AdminService {
|
|||||||
marketDataItem.symbol === symbol
|
marketDataItem.symbol === symbol
|
||||||
);
|
);
|
||||||
})?._count ?? 0;
|
})?._count ?? 0;
|
||||||
|
|
||||||
const sectorsCount = sectors ? Object.keys(sectors).length : 0;
|
const sectorsCount = sectors ? Object.keys(sectors).length : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -298,6 +339,7 @@ export class AdminService {
|
|||||||
countriesCount,
|
countriesCount,
|
||||||
dataSource,
|
dataSource,
|
||||||
id,
|
id,
|
||||||
|
lastMarketPrice,
|
||||||
name,
|
name,
|
||||||
symbol,
|
symbol,
|
||||||
marketDataItemCount,
|
marketDataItemCount,
|
||||||
@@ -511,48 +553,86 @@ export class AdminService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getMarketDataForCurrencies(): Promise<AdminMarketData> {
|
private async getMarketDataForCurrencies(): Promise<AdminMarketData> {
|
||||||
const marketDataItems = await this.prismaService.marketData.groupBy({
|
const currencyPairs = this.exchangeRateDataService.getCurrencyPairs();
|
||||||
_count: true,
|
|
||||||
by: ['dataSource', 'symbol']
|
|
||||||
});
|
|
||||||
|
|
||||||
const marketDataPromise: Promise<AdminMarketDataItem>[] =
|
const [lastMarketPrices, marketDataItems] = await Promise.all([
|
||||||
this.exchangeRateDataService
|
this.prismaService.marketData.findMany({
|
||||||
.getCurrencyPairs()
|
distinct: ['dataSource', 'symbol'],
|
||||||
.map(async ({ dataSource, symbol }) => {
|
orderBy: { date: 'desc' },
|
||||||
let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
|
select: {
|
||||||
let currency: EnhancedSymbolProfile['currency'] = '-';
|
dataSource: true,
|
||||||
let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
|
marketPrice: true,
|
||||||
|
symbol: true
|
||||||
if (isCurrency(getCurrencyFromSymbol(symbol))) {
|
},
|
||||||
currency = getCurrencyFromSymbol(symbol);
|
where: {
|
||||||
({ activitiesCount, dateOfFirstActivity } =
|
dataSource: {
|
||||||
await this.orderService.getStatisticsByCurrency(currency));
|
in: currencyPairs.map(({ dataSource }) => {
|
||||||
|
return dataSource;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
symbol: {
|
||||||
|
in: currencyPairs.map(({ symbol }) => {
|
||||||
|
return symbol;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
this.prismaService.marketData.groupBy({
|
||||||
|
_count: true,
|
||||||
|
by: ['dataSource', 'symbol']
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
const marketDataItemCount =
|
const lastMarketPriceMap = new Map<string, number>();
|
||||||
marketDataItems.find((marketDataItem) => {
|
|
||||||
return (
|
|
||||||
marketDataItem.dataSource === dataSource &&
|
|
||||||
marketDataItem.symbol === symbol
|
|
||||||
);
|
|
||||||
})?._count ?? 0;
|
|
||||||
|
|
||||||
return {
|
for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
|
||||||
activitiesCount,
|
lastMarketPriceMap.set(
|
||||||
currency,
|
getAssetProfileIdentifier({ dataSource, symbol }),
|
||||||
dataSource,
|
marketPrice
|
||||||
marketDataItemCount,
|
);
|
||||||
symbol,
|
}
|
||||||
assetClass: AssetClass.LIQUIDITY,
|
|
||||||
assetSubClass: AssetSubClass.CASH,
|
const marketDataPromise: Promise<AdminMarketDataItem>[] = currencyPairs.map(
|
||||||
countriesCount: 0,
|
async ({ dataSource, symbol }) => {
|
||||||
date: dateOfFirstActivity,
|
let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0;
|
||||||
id: undefined,
|
let currency: EnhancedSymbolProfile['currency'] = '-';
|
||||||
name: symbol,
|
let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity'];
|
||||||
sectorsCount: 0
|
|
||||||
};
|
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);
|
const marketData = await Promise.all(marketDataPromise);
|
||||||
return { marketData, count: marketData.length };
|
return { marketData, count: marketData.length };
|
||||||
|
@@ -142,6 +142,7 @@ export class AdminMarketDataComponent
|
|||||||
'dataSource',
|
'dataSource',
|
||||||
'assetClass',
|
'assetClass',
|
||||||
'assetSubClass',
|
'assetSubClass',
|
||||||
|
'lastMarketPrice',
|
||||||
'date',
|
'date',
|
||||||
'activitiesCount',
|
'activitiesCount',
|
||||||
'marketDataItemCount',
|
'marketDataItemCount',
|
||||||
|
@@ -99,6 +99,21 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="lastMarketPrice">
|
||||||
|
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||||
|
<ng-container i18n>Market Price</ng-container>
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[value]="element.lastMarketPrice ?? ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="date">
|
<ng-container matColumnDef="date">
|
||||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||||
<ng-container i18n>First Activity</ng-container>
|
<ng-container i18n>First Activity</ng-container>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter';
|
import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter';
|
||||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@@ -27,6 +28,7 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/
|
|||||||
GfCreateAssetProfileDialogModule,
|
GfCreateAssetProfileDialogModule,
|
||||||
GfPremiumIndicatorComponent,
|
GfPremiumIndicatorComponent,
|
||||||
GfSymbolModule,
|
GfSymbolModule,
|
||||||
|
GfValueComponent,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
|
@@ -16,6 +16,7 @@ export interface AdminMarketDataItem {
|
|||||||
id: string;
|
id: string;
|
||||||
isBenchmark?: boolean;
|
isBenchmark?: boolean;
|
||||||
isUsedByUsersWithSubscription?: boolean;
|
isUsedByUsersWithSubscription?: boolean;
|
||||||
|
lastMarketPrice: number;
|
||||||
marketDataItemCount: number;
|
marketDataItemCount: number;
|
||||||
name: string;
|
name: string;
|
||||||
sectorsCount: number;
|
sectorsCount: number;
|
||||||
|
Reference in New Issue
Block a user