diff --git a/CHANGELOG.md b/CHANGELOG.md index e57661406..2034a8f9d 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 calculation for developed vs. emerging markets to the allocations page - Added a hover effect to the page tabs - Extended the feature overview page by _Bonds_ and _Emergency Fund_ diff --git a/apps/api/src/app/portfolio/portfolio-service.strategy.ts b/apps/api/src/app/portfolio/portfolio-service.strategy.ts index 49ec0422f..a85b28852 100644 --- a/apps/api/src/app/portfolio/portfolio-service.strategy.ts +++ b/apps/api/src/app/portfolio/portfolio-service.strategy.ts @@ -13,8 +13,9 @@ export class PortfolioServiceStrategy { @Inject(REQUEST) private readonly request: RequestWithUser ) {} - public get() { + public get(newCalculationEngine?: boolean) { if ( + newCalculationEngine || this.request.user?.Settings?.settings?.['isNewCalculationEngine'] === true ) { return this.portfolioServiceNew; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 2d7993859..1bb42a0ed 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -120,7 +120,7 @@ export class PortfolioController { const { accounts, holdings, hasErrors } = await this.portfolioServiceStrategy - .get() + .get(true) .getDetails(impersonationId, this.request.user.id, range); if (hasErrors || hasNotDefinedValuesInObject(holdings)) { @@ -277,7 +277,7 @@ export class PortfolioController { } const { holdings } = await this.portfolioServiceStrategy - .get() + .get(true) .getDetails(access.userId, access.userId); const portfolioPublicDetails: PortfolioPublicDetails = { @@ -304,6 +304,7 @@ export class PortfolioController { allocationCurrent: portfolioPosition.allocationCurrent, countries: hasDetails ? portfolioPosition.countries : [], currency: portfolioPosition.currency, + markets: portfolioPosition.markets, name: portfolioPosition.name, sectors: hasDetails ? portfolioPosition.sectors : [], value: portfolioPosition.value / totalValue diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index 2ea49de8f..63b8544a8 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -40,6 +40,7 @@ import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.in import type { AccountWithValue, DateRange, + Market, OrderWithAccount, RequestWithUser } from '@ghostfolio/common/types'; @@ -71,6 +72,9 @@ import { import { PortfolioCalculatorNew } from './portfolio-calculator-new'; import { RulesService } from './rules.service'; +const developedMarkets = require('../../assets/countries/developed-markets.json'); +const emergingMarkets = require('../../assets/countries/emerging-markets.json'); + @Injectable() export class PortfolioServiceNew { public constructor( @@ -380,7 +384,31 @@ export class PortfolioServiceNew { const value = item.quantity.mul(item.marketPrice); const symbolProfile = symbolProfileMap[item.symbol]; const dataProviderResponse = dataProviderResponses[item.symbol]; + + const markets: { [key in Market]: number } = { + developedMarkets: 0, + emergingMarkets: 0, + otherMarkets: 0 + }; + + for (const country of symbolProfile.countries) { + if (developedMarkets.includes(country.code)) { + markets.developedMarkets = new Big(markets.developedMarkets) + .plus(country.weight) + .toNumber(); + } else if (emergingMarkets.includes(country.code)) { + markets.emergingMarkets = new Big(markets.emergingMarkets) + .plus(country.weight) + .toNumber(); + } else { + markets.otherMarkets = new Big(markets.otherMarkets) + .plus(country.weight) + .toNumber(); + } + } + holdings[item.symbol] = { + markets, allocationCurrent: value.div(totalValue).toNumber(), allocationInvestment: item.investment.div(totalInvestment).toNumber(), assetClass: symbolProfile.assetClass, diff --git a/apps/api/src/assets/countries/developed-markets.json b/apps/api/src/assets/countries/developed-markets.json new file mode 100644 index 000000000..5e281d475 --- /dev/null +++ b/apps/api/src/assets/countries/developed-markets.json @@ -0,0 +1,26 @@ +[ + "AT", + "AU", + "BE", + "CA", + "CH", + "DE", + "DK", + "ES", + "FI", + "FR", + "GB", + "HK", + "IE", + "IL", + "IT", + "JP", + "LU", + "NL", + "NO", + "NZ", + "PT", + "SE", + "SG", + "US" +] diff --git a/apps/api/src/assets/countries/emerging-markets.json b/apps/api/src/assets/countries/emerging-markets.json new file mode 100644 index 000000000..328187964 --- /dev/null +++ b/apps/api/src/assets/countries/emerging-markets.json @@ -0,0 +1,28 @@ +[ + "AE", + "BR", + "CL", + "CN", + "CO", + "CY", + "CZ", + "EG", + "GR", + "HK", + "HU", + "ID", + "IN", + "KR", + "KW", + "MX", + "MY", + "PE", + "PH", + "PL", + "QA", + "SA", + "TH", + "TR", + "TW", + "ZA" +] diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index 2ca5f6930..5d7be99c4 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -14,7 +14,7 @@ import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import { ToggleOption } from '@ghostfolio/common/types'; +import { Market, ToggleOption } from '@ghostfolio/common/types'; import { Account, AssetClass, DataSource } from '@prisma/client'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, Subscription } from 'rxjs'; @@ -42,6 +42,9 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { public deviceType: string; public hasImpersonationId: boolean; public hasPermissionToCreateOrder: boolean; + public markets: { + [key in Market]: { name: string; value: number }; + }; public period = 'current'; public periodOptions: ToggleOption[] = [ { label: 'Initial', value: 'original' }, @@ -160,6 +163,20 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { value: 0 } }; + this.markets = { + developedMarkets: { + name: 'developedMarkets', + value: 0 + }, + emergingMarkets: { + name: 'emergingMarkets', + value: 0 + }, + otherMarkets: { + name: 'otherMarkets', + value: 0 + } + }; this.positions = {}; this.positionsArray = []; this.sectors = { @@ -219,6 +236,16 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { // Prepare analysis data by continents, countries and sectors except for cash if (position.countries.length > 0) { + this.markets.developedMarkets.value += + position.markets.developedMarkets * + (aPeriod === 'original' ? position.investment : position.value); + this.markets.emergingMarkets.value += + position.markets.emergingMarkets * + (aPeriod === 'original' ? position.investment : position.value); + this.markets.otherMarkets.value += + position.markets.otherMarkets * + (aPeriod === 'original' ? position.investment : position.value); + for (const country of position.countries) { const { code, continent, name, weight } = country; @@ -294,6 +321,18 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { }; } } + + const marketsTotal = + this.markets.developedMarkets.value + + this.markets.emergingMarkets.value + + this.markets.otherMarkets.value; + + this.markets.developedMarkets.value = + this.markets.developedMarkets.value / marketsTotal; + this.markets.emergingMarkets.value = + this.markets.emergingMarkets.value / marketsTotal; + this.markets.otherMarkets.value = + this.markets.otherMarkets.value / marketsTotal; } public onChangePeriod(aValue: string) { diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html index 276de32ce..679570998 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -190,6 +190,32 @@ [countries]="countries" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView" > +
+
+ +
+
+ +
+
+ +
+
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts index 809b29bd7..62dd05e4e 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.module.ts @@ -5,6 +5,7 @@ import { GfPositionsTableModule } from '@ghostfolio/client/components/positions- import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module'; import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module'; import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; +import { GfValueModule } from '@ghostfolio/ui/value'; import { AllocationsPageRoutingModule } from './allocations-page-routing.module'; import { AllocationsPageComponent } from './allocations-page.component'; @@ -19,6 +20,7 @@ import { AllocationsPageComponent } from './allocations-page.component'; GfPositionsTableModule, GfToggleModule, GfWorldMapChartModule, + GfValueModule, MatCardModule ], providers: [], diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts index a3ac43721..aea98e471 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -7,6 +7,7 @@ import { PortfolioPosition, PortfolioPublicDetails } from '@ghostfolio/common/interfaces'; +import { Market } from '@ghostfolio/common/types'; import { StatusCodes } from 'http-status-codes'; import { DeviceDetectorService } from 'ngx-device-detector'; import { EMPTY, Subject } from 'rxjs'; @@ -26,6 +27,9 @@ export class PublicPageComponent implements OnInit { [code: string]: { name: string; value: number }; }; public deviceType: string; + public markets: { + [key in Market]: { name: string; value: number }; + }; public portfolioPublicDetails: PortfolioPublicDetails; public positions: { [symbol: string]: Pick; @@ -96,6 +100,20 @@ export class PublicPageComponent implements OnInit { value: 0 } }; + this.markets = { + developedMarkets: { + name: 'developedMarkets', + value: 0 + }, + emergingMarkets: { + name: 'emergingMarkets', + value: 0 + }, + otherMarkets: { + name: 'otherMarkets', + value: 0 + } + }; this.positions = {}; this.sectors = { [UNKNOWN_KEY]: { @@ -123,6 +141,13 @@ export class PublicPageComponent implements OnInit { }; if (position.countries.length > 0) { + this.markets.developedMarkets.value += + position.markets.developedMarkets * position.value; + this.markets.emergingMarkets.value += + position.markets.emergingMarkets * position.value; + this.markets.otherMarkets.value += + position.markets.otherMarkets * position.value; + for (const country of position.countries) { const { code, continent, name, weight } = country; @@ -176,6 +201,18 @@ export class PublicPageComponent implements OnInit { value: position.value }; } + + const marketsTotal = + this.markets.developedMarkets.value + + this.markets.emergingMarkets.value + + this.markets.otherMarkets.value; + + this.markets.developedMarkets.value = + this.markets.developedMarkets.value / marketsTotal; + this.markets.emergingMarkets.value = + this.markets.emergingMarkets.value / marketsTotal; + this.markets.otherMarkets.value = + this.markets.otherMarkets.value / marketsTotal; } public ngOnDestroy() { diff --git a/apps/client/src/app/pages/public/public-page.html b/apps/client/src/app/pages/public/public-page.html index 49edef614..5af657d67 100644 --- a/apps/client/src/app/pages/public/public-page.html +++ b/apps/client/src/app/pages/public/public-page.html @@ -79,12 +79,38 @@ [countries]="countries" [isInPercent]="true" > +
+
+ +
+
+ +
+
+ +
+
-
+

Would you like to refine your personal investment strategy? diff --git a/apps/client/src/app/pages/public/public-page.module.ts b/apps/client/src/app/pages/public/public-page.module.ts index e7818ede2..21a3b4f7b 100644 --- a/apps/client/src/app/pages/public/public-page.module.ts +++ b/apps/client/src/app/pages/public/public-page.module.ts @@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module'; import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; +import { GfValueModule } from '@ghostfolio/ui/value'; import { PublicPageRoutingModule } from './public-page-routing.module'; import { PublicPageComponent } from './public-page.component'; @@ -14,6 +15,7 @@ import { PublicPageComponent } from './public-page.component'; imports: [ CommonModule, GfPortfolioProportionChartModule, + GfValueModule, GfWorldMapChartModule, MatButtonModule, MatCardModule, diff --git a/libs/common/src/lib/interfaces/portfolio-position.interface.ts b/libs/common/src/lib/interfaces/portfolio-position.interface.ts index 87ee917d4..1b5714a9e 100644 --- a/libs/common/src/lib/interfaces/portfolio-position.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-position.interface.ts @@ -1,6 +1,7 @@ import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces'; import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; +import { Market } from '../types'; import { Country } from './country.interface'; import { Sector } from './sector.interface'; @@ -19,6 +20,7 @@ export interface PortfolioPosition { marketChange?: number; marketChangePercent?: number; marketPrice: number; + markets?: { [key in Market]: number }; marketState: MarketState; name: string; netPerformance: number; 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 b24df034e..10fb4edfd 100644 --- a/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-public-details.interface.ts @@ -8,6 +8,7 @@ export interface PortfolioPublicDetails { | 'allocationCurrent' | 'countries' | 'currency' + | 'markets' | 'name' | 'sectors' | 'value' diff --git a/libs/common/src/lib/types/index.ts b/libs/common/src/lib/types/index.ts index 6953b5863..40ba51272 100644 --- a/libs/common/src/lib/types/index.ts +++ b/libs/common/src/lib/types/index.ts @@ -2,6 +2,7 @@ import type { AccessWithGranteeUser } from './access-with-grantee-user.type'; import { AccountWithValue } from './account-with-value.type'; import type { DateRange } from './date-range.type'; import type { Granularity } from './granularity.type'; +import { Market } from './market.type'; import type { OrderWithAccount } from './order-with-account.type'; import type { RequestWithUser } from './request-with-user.type'; import { ToggleOption } from './toggle-option.type'; @@ -11,6 +12,7 @@ export type { AccountWithValue, DateRange, Granularity, + Market, OrderWithAccount, RequestWithUser, ToggleOption diff --git a/libs/common/src/lib/types/market.type.ts b/libs/common/src/lib/types/market.type.ts new file mode 100644 index 000000000..d6981d256 --- /dev/null +++ b/libs/common/src/lib/types/market.type.ts @@ -0,0 +1 @@ +export type Market = 'developedMarkets' | 'emergingMarkets' | 'otherMarkets'; diff --git a/libs/ui/src/lib/value/value.component.html b/libs/ui/src/lib/value/value.component.html index e4f8b0f45..026eb2794 100644 --- a/libs/ui/src/lib/value/value.component.html +++ b/libs/ui/src/lib/value/value.component.html @@ -43,9 +43,14 @@

- - {{ label }} - + +
+ {{ label }} +
+ + {{ label }} + +