From 5596e5f03b74203999f1182d5e208d81ad41f78a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 2 Mar 2024 14:29:03 +0100 Subject: [PATCH] Feature/integrate wealth items into transaction point concept (#3084) * Integrate (wealth) items into transaction point concept * Update changelog --- CHANGELOG.md | 2 + apps/api/src/app/import/import.controller.ts | 2 +- apps/api/src/app/order/order.service.ts | 13 +++++- .../portfolio/current-rate.service.spec.ts | 1 + .../src/app/portfolio/current-rate.service.ts | 10 ++++- ...folio-calculator-baln-buy-and-sell.spec.ts | 2 +- .../portfolio-calculator-baln-buy.spec.ts | 2 +- ...ator-btcusd-buy-and-sell-partially.spec.ts | 2 +- .../portfolio-calculator-googl-buy.spec.ts | 2 +- .../portfolio-calculator-no-orders.spec.ts | 2 +- ...ulator-novn-buy-and-sell-partially.spec.ts | 2 +- ...folio-calculator-novn-buy-and-sell.spec.ts | 2 +- .../portfolio/portfolio-calculator.spec.ts | 2 +- .../src/app/portfolio/portfolio-calculator.ts | 1 + .../src/app/portfolio/portfolio.controller.ts | 7 +++- .../src/app/portfolio/portfolio.service.ts | 40 ++++++++++--------- apps/api/src/app/symbol/symbol.controller.ts | 2 +- .../data-provider/manual/manual.service.ts | 14 ++++--- .../account-detail-dialog.component.ts | 3 +- apps/client/src/app/services/data.service.ts | 8 +++- .../portfolio-public-details.interface.ts | 2 +- 21 files changed, 80 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 412ce06e1..85485a7fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the usability of the benchmarks in the markets overview +- Integrated (wealth) items into the transaction point concept in the portfolio service ### Fixed - Fixed a missing value in the activities table on mobile +- Fixed a missing value on the public page - Displayed the button to fetch the current market price only if the activity is from today ## 2.59.0 - 2024-02-29 diff --git a/apps/api/src/app/import/import.controller.ts b/apps/api/src/app/import/import.controller.ts index 3a79a7409..b7aff8634 100644 --- a/apps/api/src/app/import/import.controller.ts +++ b/apps/api/src/app/import/import.controller.ts @@ -43,7 +43,7 @@ export class ImportController { @UseInterceptors(TransformDataSourceInResponseInterceptor) public async import( @Body() importData: ImportDataDto, - @Query('dryRun') isDryRun?: boolean + @Query('dryRun') isDryRun = false ): Promise { if ( !hasPermission(this.request.user.permissions, permissions.createAccount) diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index a88aa4462..79738c27e 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -8,7 +8,7 @@ import { GATHER_ASSET_PROFILE_PROCESS_OPTIONS } from '@ghostfolio/common/config'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; -import { Filter } from '@ghostfolio/common/interfaces'; +import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; @@ -200,6 +200,17 @@ export class OrderService { return count; } + public async getLatestOrder({ dataSource, symbol }: UniqueAsset) { + return this.prismaService.order.findFirst({ + orderBy: { + date: 'desc' + }, + where: { + SymbolProfile: { dataSource, symbol } + } + }); + } + public async getOrders({ filters, includeDrafts = false, diff --git a/apps/api/src/app/portfolio/current-rate.service.spec.ts b/apps/api/src/app/portfolio/current-rate.service.spec.ts index d87ee7ccc..9b0548522 100644 --- a/apps/api/src/app/portfolio/current-rate.service.spec.ts +++ b/apps/api/src/app/portfolio/current-rate.service.spec.ts @@ -108,6 +108,7 @@ describe('CurrentRateService', () => { currentRateService = new CurrentRateService( dataProviderService, marketDataService, + null, null ); }); diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index 73cad45e6..f4855329e 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -1,3 +1,4 @@ +import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { resetHours } from '@ghostfolio/common/helper'; @@ -22,6 +23,7 @@ export class CurrentRateService { public constructor( private readonly dataProviderService: DataProviderService, private readonly marketDataService: MarketDataService, + private readonly orderService: OrderService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} @@ -121,11 +123,17 @@ export class CurrentRateService { }); if (!value) { + // Fallback to unit price of latest activity + const latestActivity = await this.orderService.getLatestOrder({ + dataSource, + symbol + }); + value = { dataSource, symbol, date: today, - marketPrice: 0 + marketPrice: latestActivity?.unitPrice ?? 0 }; response.values.push(value); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts index 60956a9d6..d81393719 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => { let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( null, diff --git a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts index ecbe2851c..e77335ab8 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts @@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => { let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( null, diff --git a/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index c1e3a71c7..8f5573928 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -34,7 +34,7 @@ describe('PortfolioCalculator', () => { let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( null, diff --git a/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts index 2a6eeaedb..502248388 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts @@ -34,7 +34,7 @@ describe('PortfolioCalculator', () => { let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( null, diff --git a/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts index bc860b2aa..ab7234822 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts @@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => { let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( null, diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index d73db8c2c..c46fd54d2 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => { let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( null, diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts index 253b47346..fa3ebac9b 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -21,7 +21,7 @@ describe('PortfolioCalculator', () => { let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( null, diff --git a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts index dfe3f3466..a59b877ab 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts @@ -10,7 +10,7 @@ describe('PortfolioCalculator', () => { let exchangeRateDataService: ExchangeRateDataService; beforeEach(() => { - currentRateService = new CurrentRateService(null, null, null); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( null, diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 439b39fd7..9b76aa735 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -825,6 +825,7 @@ export class PortfolioCalculator { switch (type) { case 'BUY': + case 'ITEM': factor = 1; break; case 'SELL': diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 57f7da56b..8f4acc060 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -342,7 +342,8 @@ export class PortfolioController { @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', @Query('tags') filterByTags?: string, - @Query('withExcludedAccounts') withExcludedAccounts = false + @Query('withExcludedAccounts') withExcludedAccounts = false, + @Query('withItems') withItems = false ): Promise { const hasReadRestrictedAccessPermission = this.userService.hasReadRestrictedAccessPermission({ @@ -361,6 +362,7 @@ export class PortfolioController { filters, impersonationId, withExcludedAccounts, + withItems, userId: this.request.user.id }); @@ -515,7 +517,8 @@ export class PortfolioController { dateOfFirstActivity: portfolioPosition.dateOfFirstActivity, markets: hasDetails ? portfolioPosition.markets : undefined, name: portfolioPosition.name, - netPerformancePercent: portfolioPosition.netPerformancePercent, + netPerformancePercentWithCurrencyEffect: + portfolioPosition.netPerformancePercentWithCurrencyEffect, sectors: hasDetails ? portfolioPosition.sectors : [], symbol: portfolioPosition.symbol, url: portfolioPosition.url, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 44e1c5524..331f75f5f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -277,7 +277,8 @@ export class PortfolioService { await this.getTransactionPoints({ filters, userId, - includeDrafts: true + includeDrafts: true, + types: ['BUY', 'SELL'] }); if (transactionPoints.length === 0) { @@ -702,7 +703,7 @@ export class PortfolioService { .filter((order) => { tags = tags.concat(order.tags); - return order.type === 'BUY' || order.type === 'SELL'; + return ['BUY', 'ITEM', 'SELL'].includes(order.type); }) .map((order) => ({ currency: order.SymbolProfile.currency, @@ -957,7 +958,8 @@ export class PortfolioService { const { portfolioOrders, transactionPoints } = await this.getTransactionPoints({ filters, - userId + userId, + types: ['BUY', 'SELL'] }); if (transactionPoints?.length <= 0) { @@ -1087,13 +1089,15 @@ export class PortfolioService { filters, impersonationId, userId, - withExcludedAccounts = false + withExcludedAccounts = false, + withItems = false }: { dateRange?: DateRange; filters?: Filter[]; impersonationId: string; userId: string; withExcludedAccounts?: boolean; + withItems?: boolean; }): Promise { userId = await this.getUserId(impersonationId, userId); const user = await this.userService.user({ id: userId }); @@ -1128,7 +1132,8 @@ export class PortfolioService { await this.getTransactionPoints({ filters, userId, - withExcludedAccounts + withExcludedAccounts, + types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL'] }); const portfolioCalculator = new PortfolioCalculator({ @@ -1280,7 +1285,8 @@ export class PortfolioService { const { orders, portfolioOrders, transactionPoints } = await this.getTransactionPoints({ - userId + userId, + types: ['BUY', 'SELL'] }); const portfolioCalculator = new PortfolioCalculator({ @@ -1913,11 +1919,13 @@ export class PortfolioService { private async getTransactionPoints({ filters, includeDrafts = false, + types = ['BUY', 'ITEM', 'SELL'], userId, withExcludedAccounts = false }: { filters?: Filter[]; includeDrafts?: boolean; + types?: ActivityType[]; userId: string; withExcludedAccounts?: boolean; }): Promise<{ @@ -1931,10 +1939,10 @@ export class PortfolioService { const { activities, count } = await this.orderService.getOrders({ filters, includeDrafts, + types, userCurrency, userId, - withExcludedAccounts, - types: ['BUY', 'SELL'] + withExcludedAccounts }); if (count <= 0) { @@ -2006,7 +2014,7 @@ export class PortfolioService { userCurrency, userId, withExcludedAccounts, - types: ['ITEM', 'LIABILITY'] + types: ['LIABILITY'] }); const accounts: PortfolioDetails['accounts'] = {}; @@ -2094,18 +2102,14 @@ export class PortfolioService { Account, quantity, SymbolProfile, - type, - valueInBaseCurrency + type } of ordersByAccount) { - const unitPriceInBaseCurrency = - portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ?? - valueInBaseCurrency ?? - 0; - let currentValueOfSymbolInBaseCurrency = - quantity * unitPriceInBaseCurrency; + quantity * + portfolioItemsNow[SymbolProfile.symbol] + ?.marketPriceInBaseCurrency ?? 0; - if (type === 'LIABILITY' || type === 'SELL') { + if (['LIABILITY', 'SELL'].includes(type)) { currentValueOfSymbolInBaseCurrency *= -1; } diff --git a/apps/api/src/app/symbol/symbol.controller.ts b/apps/api/src/app/symbol/symbol.controller.ts index 5b50599cf..e41267b79 100644 --- a/apps/api/src/app/symbol/symbol.controller.ts +++ b/apps/api/src/app/symbol/symbol.controller.ts @@ -39,7 +39,7 @@ export class SymbolController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInResponseInterceptor) public async lookupSymbol( - @Query('includeIndices') includeIndices: boolean = false, + @Query('includeIndices') includeIndices = false, @Query('query') query = '' ): Promise<{ items: LookupItem[] }> { try { diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts index bc76645eb..4ff3ba653 100644 --- a/apps/api/src/services/data-provider/manual/manual.service.ts +++ b/apps/api/src/services/data-provider/manual/manual.service.ts @@ -166,13 +166,15 @@ export class ManualService implements DataProviderInterface { } }); - for (const symbolProfile of symbolProfiles) { - response[symbolProfile.symbol] = { - currency: symbolProfile.currency, + for (const { currency, symbol } of symbolProfiles) { + let marketPrice = marketData.find((marketDataItem) => { + return marketDataItem.symbol === symbol; + })?.marketPrice; + + response[symbol] = { + currency, + marketPrice, dataSource: this.getName(), - marketPrice: marketData.find((marketDataItem) => { - return marketDataItem.symbol === symbolProfile.symbol; - })?.marketPrice, marketState: 'delayed' }; } diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index 0c61cf1d3..2cd48a561 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -227,7 +227,8 @@ export class AccountDetailDialog implements OnDestroy, OnInit { } ], range: 'max', - withExcludedAccounts: true + withExcludedAccounts: true, + withItems: true }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ chart }) => { diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index d36aca152..6555a964d 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -437,11 +437,13 @@ export class DataService { public fetchPortfolioPerformance({ filters, range, - withExcludedAccounts = false + withExcludedAccounts = false, + withItems = false }: { filters?: Filter[]; range: DateRange; withExcludedAccounts?: boolean; + withItems?: boolean; }): Observable { let params = this.buildFiltersAsQueryParams({ filters }); params = params.append('range', range); @@ -450,6 +452,10 @@ export class DataService { params = params.append('withExcludedAccounts', withExcludedAccounts); } + if (withItems) { + params = params.append('withItems', withItems); + } + return this.http .get(`/api/v2/portfolio/performance`, { params diff --git a/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts b/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts index fb56ed94d..57b0b36cc 100644 --- a/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts @@ -13,7 +13,7 @@ export interface PortfolioPublicDetails { | 'dateOfFirstActivity' | 'markets' | 'name' - | 'netPerformancePercent' + | 'netPerformancePercentWithCurrencyEffect' | 'sectors' | 'symbol' | 'url'