diff --git a/CHANGELOG.md b/CHANGELOG.md index 05355f453..dd2ec27a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added support for bonds in the import dividends dialog - Added a _Copy link to clipboard_ action to the access table to share the portfolio +- 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 +- Improved the usability of the toggle component - Switched to the accounts endpoint in the holding detail dialog ## 2.107.1 - 2024-09-12 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/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts index 3ce23a3bc..79e4d40dc 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts @@ -5,10 +5,9 @@ import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; -import { Account, Tag } from '@prisma/client'; +import { Tag } from '@prisma/client'; export interface PortfolioHoldingDetail { - accounts: Account[]; averagePrice: number; dataProviderInfo: DataProviderInfo; dividendInBaseCurrency: number; diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index e1dfc888d..39ac9cc6f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -73,7 +73,7 @@ import { parseISO, set } from 'date-fns'; -import { isEmpty, last, uniq, uniqBy } from 'lodash'; +import { isEmpty, last, uniq } from 'lodash'; import { PortfolioCalculator } from './calculator/portfolio-calculator'; import { @@ -625,7 +625,6 @@ export class PortfolioService { if (activities.length === 0) { return { - accounts: [], averagePrice: undefined, dataProviderInfo: undefined, dividendInBaseCurrency: undefined, @@ -699,15 +698,6 @@ export class PortfolioService { ); }); - const accounts: PortfolioHoldingDetail['accounts'] = uniqBy( - activitiesOfPosition.filter(({ Account }) => { - return Account; - }), - 'Account.id' - ).map(({ Account }) => { - return Account; - }); - const dividendYieldPercent = getAnnualizedPerformancePercent({ daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), netPerformancePercentage: timeWeightedInvestment.eq(0) @@ -788,7 +778,6 @@ export class PortfolioService { } return { - accounts, firstBuyDate, marketPrice, maxPrice, @@ -883,7 +872,6 @@ export class PortfolioService { maxPrice, minPrice, SymbolProfile, - accounts: [], averagePrice: 0, dataProviderInfo: undefined, dividendInBaseCurrency: 0, diff --git a/apps/api/src/app/portfolio/rules.service.ts b/apps/api/src/app/portfolio/rules.service.ts index 6b6526144..fd9d794b2 100644 --- a/apps/api/src/app/portfolio/rules.service.ts +++ b/apps/api/src/app/portfolio/rules.service.ts @@ -1,6 +1,9 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { Rule } from '@ghostfolio/api/models/rule'; -import { UserSettings } from '@ghostfolio/common/interfaces'; +import { + PortfolioReportRule, + UserSettings +} from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; @@ -11,19 +14,23 @@ export class RulesService { public async evaluate( aRules: Rule[], aUserSettings: UserSettings - ) { + ): Promise { return aRules.map((rule) => { - if (rule.getSettings(aUserSettings)?.isActive) { - const { evaluation, value } = rule.evaluate( - rule.getSettings(aUserSettings) - ); + const settings = rule.getSettings(aUserSettings); + + if (settings?.isActive) { + const { evaluation, value } = rule.evaluate(settings); return { evaluation, value, isActive: true, key: rule.getKey(), - name: rule.getName() + name: rule.getName(), + settings: { + thresholdMax: settings['thresholdMax'], + thresholdMin: settings['thresholdMin'] + } }; } else { return { 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/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts new file mode 100644 index 000000000..a409ab503 --- /dev/null +++ b/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts @@ -0,0 +1,5 @@ +import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; + +export interface IRuleSettingsDialogParams { + rule: PortfolioReportRule; +} diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts new file mode 100644 index 000000000..41ebf49db --- /dev/null +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts @@ -0,0 +1,40 @@ +import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; + +import { CommonModule } from '@angular/common'; +import { Component, Inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { + MAT_DIALOG_DATA, + MatDialogModule, + MatDialogRef +} from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; + +import { IRuleSettingsDialogParams } from './interfaces/interfaces'; + +@Component({ + imports: [ + CommonModule, + MatButtonModule, + MatDialogModule, + MatFormFieldModule, + MatInputModule + ], + selector: 'gf-rule-settings-dialog', + standalone: true, + styleUrls: ['./rule-settings-dialog.scss'], + templateUrl: './rule-settings-dialog.html' +}) +export class GfRuleSettingsDialogComponent { + public settings: PortfolioReportRule['settings']; + + public constructor( + @Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams, + public dialogRef: MatDialogRef + ) { + console.log(this.data.rule); + + this.settings = this.data.rule.settings; + } +} diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html new file mode 100644 index 000000000..e24db29f7 --- /dev/null +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html @@ -0,0 +1,23 @@ +
{{ data.rule.name }}
+ +
+ + Threshold Min + + + + Threshold Max + + +
+ +
+ + +
diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.scss b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.scss new file mode 100644 index 000000000..dc9093b45 --- /dev/null +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.scss @@ -0,0 +1,2 @@ +:host { +} diff --git a/apps/client/src/app/components/rule/rule.component.html b/apps/client/src/app/components/rule/rule.component.html index 80b442b7b..f19436aba 100644 --- a/apps/client/src/app/components/rule/rule.component.html +++ b/apps/client/src/app/components/rule/rule.component.html @@ -62,6 +62,11 @@ + @if (rule?.isActive && !isEmpty(rule.settings) && false) { + + }