diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index b0f7bb2c3..310daa9b9 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -57,7 +57,7 @@ export class PublicController { } const [ - { holdings }, + { holdings, markets }, { performance: performance1d }, { performance: performanceMax }, { performance: performanceYtd } @@ -80,6 +80,7 @@ export class PublicController { hasDetails, alias: access.alias, holdings: {}, + markets: hasDetails ? markets : undefined, performance: { '1d': { relativeChange: diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 9f5635cf5..0d1d8a0a8 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -1,4 +1,3 @@ -import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; @@ -61,7 +60,6 @@ import { UpdateHoldingTagsDto } from './update-holding-tags.dto'; @Controller('portfolio') export class PortfolioController { public constructor( - private readonly accessService: AccessService, private readonly apiService: ApiService, private readonly configurationService: ConfigurationService, private readonly impersonationService: ImpersonationService, @@ -97,7 +95,7 @@ export class PortfolioController { filterByTags }); - const { accounts, hasErrors, holdings, platforms, summary } = + const { accounts, hasErrors, holdings, markets, platforms, summary } = await this.portfolioService.getDetails({ dateRange, filters, @@ -216,7 +214,8 @@ export class PortfolioController { hasError, holdings, platforms, - summary: portfolioSummary + summary: portfolioSummary, + markets: hasDetails ? markets : undefined }; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 7aae6f8d9..0be908030 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -45,6 +45,7 @@ import type { AccountWithValue, DateRange, GroupBy, + Market, RequestWithUser, UserWithSettings } from '@ghostfolio/common/types'; @@ -73,7 +74,7 @@ import { parseISO, set } from 'date-fns'; -import { isEmpty, last, uniq } from 'lodash'; +import { isEmpty, isNumber, last, uniq } from 'lodash'; import { PortfolioCalculator } from './calculator/portfolio-calculator'; import { @@ -581,6 +582,17 @@ export class PortfolioService { }; } + let markets: { + [key in Market]: { + name: string; + value: number; + }; + }; + + if (withMarkets) { + markets = this.getAggregatedMarkets(holdings); + } + let summary: PortfolioSummary; if (withSummary) { @@ -602,6 +614,7 @@ export class PortfolioService { accounts, hasErrors, holdings, + markets, platforms, summary }; @@ -1232,6 +1245,62 @@ export class PortfolioService { await this.orderService.assignTags({ dataSource, symbol, tags, userId }); } + private getAggregatedMarkets(holdings: { + [symbol: string]: PortfolioPosition; + }): { + [key in Market]: { name: string; value: number }; + } { + const markets = { + [UNKNOWN_KEY]: { + name: UNKNOWN_KEY, + value: 0 + }, + developedMarkets: { + name: 'developedMarkets', + value: 0 + }, + emergingMarkets: { + name: 'emergingMarkets', + value: 0 + }, + otherMarkets: { + name: 'otherMarkets', + value: 0 + } + }; + + for (const [symbol, position] of Object.entries(holdings)) { + const value = position.valueInBaseCurrency; + + if (position.assetClass !== AssetClass.LIQUIDITY) { + if (position.countries.length > 0) { + markets.developedMarkets.value += + position.markets.developedMarkets * value; + markets.emergingMarkets.value += + position.markets.emergingMarkets * value; + markets.otherMarkets.value += position.markets.otherMarkets * value; + } else { + markets[UNKNOWN_KEY].value += value; + } + } + } + + const marketsTotal = + markets.developedMarkets.value + + markets.emergingMarkets.value + + markets.otherMarkets.value + + markets[UNKNOWN_KEY].value; + + markets.developedMarkets.value = + markets.developedMarkets.value / marketsTotal; + markets.emergingMarkets.value = + markets.emergingMarkets.value / marketsTotal; + markets.otherMarkets.value = markets.otherMarkets.value / marketsTotal; + markets[UNKNOWN_KEY].value = markets[UNKNOWN_KEY].value / marketsTotal; + + return markets; + } + private async getCashPositions({ cashDetails, userCurrency, diff --git a/libs/common/src/lib/interfaces/portfolio-details.interface.ts b/libs/common/src/lib/interfaces/portfolio-details.interface.ts index 611ed8056..3c2833071 100644 --- a/libs/common/src/lib/interfaces/portfolio-details.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-details.interface.ts @@ -2,6 +2,7 @@ import { PortfolioPosition, PortfolioSummary } from '@ghostfolio/common/interfaces'; +import { Market } from '@ghostfolio/common/types'; export interface PortfolioDetails { accounts: { @@ -14,6 +15,12 @@ export interface PortfolioDetails { }; }; holdings: { [symbol: string]: PortfolioPosition }; + markets?: { + [key in Market]: { + name: string; + value: number; + }; + }; platforms: { [id: string]: { balance: number; diff --git a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts index ce623a058..0e34d08db 100644 --- a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts @@ -1,3 +1,5 @@ +import { Market } from '@ghostfolio/common/types'; + import { PortfolioPosition } from '../portfolio-position.interface'; export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { @@ -22,6 +24,12 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { | 'valueInPercentage' >; }; + markets?: { + [key in Market]: { + name: string; + value: number; + }; + }; } interface PublicPortfolioResponseV1 {