From 719bbe156e081b04d12cc85fce47152d13ca52b2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 7 Apr 2024 09:26:30 +0200 Subject: [PATCH] Feature/optimize calculation of allocations by market (#3249) * Optimize calculation of allocations by market * Update changelog --- CHANGELOG.md | 1 + .../src/app/portfolio/portfolio.controller.ts | 5 +- .../src/app/portfolio/portfolio.service.ts | 175 ++++++++++-------- .../allocations/allocations-page.component.ts | 3 +- apps/client/src/app/services/data.service.ts | 8 +- 5 files changed, 113 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 932206f9..19140a7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Optimized the calculation of allocations by market - Improved the url validation in the create and update platform endpoint - Improved the language localization for German (`de`) diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 6047b7ab..0ae596d8 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -78,9 +78,11 @@ export class PortfolioController { @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', @Query('tags') filterByTags?: string, - @Query('withLiabilities') withLiabilitiesParam = 'false' + @Query('withLiabilities') withLiabilitiesParam = 'false', + @Query('withMarkets') withMarketsParam = 'false' ): Promise { const withLiabilities = withLiabilitiesParam === 'true'; + const withMarkets = withMarketsParam === 'true'; let hasDetails = true; let hasError = false; @@ -106,6 +108,7 @@ export class PortfolioController { filters, impersonationId, withLiabilities, + withMarkets, userId: this.request.user.id, withSummary: true }); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index fc13ea8e..99fb47e2 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -63,7 +63,8 @@ import { DataSource, Order, Platform, - Prisma + Prisma, + SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; import { isUUID } from 'class-validator'; @@ -337,6 +338,7 @@ export class PortfolioService { userId, withExcludedAccounts = false, withLiabilities = false, + withMarkets = false, withSummary = false }: { dateRange?: DateRange; @@ -345,6 +347,7 @@ export class PortfolioService { userId: string; withExcludedAccounts?: boolean; withLiabilities?: boolean; + withMarkets?: boolean; withSummary?: boolean; }): Promise { userId = await this.getUserId(impersonationId, userId); @@ -484,77 +487,17 @@ export class PortfolioService { } } - const symbolProfile = symbolProfileMap[symbol]; + const assetProfile = symbolProfileMap[symbol]; const dataProviderResponse = dataProviderResponses[symbol]; - const markets: PortfolioPosition['markets'] = { - [UNKNOWN_KEY]: 0, - developedMarkets: 0, - emergingMarkets: 0, - otherMarkets: 0 - }; - const marketsAdvanced: PortfolioPosition['marketsAdvanced'] = { - [UNKNOWN_KEY]: 0, - asiaPacific: 0, - emergingMarkets: 0, - europe: 0, - japan: 0, - northAmerica: 0, - otherMarkets: 0 - }; + let markets: PortfolioPosition['markets']; + let marketsAdvanced: PortfolioPosition['marketsAdvanced']; - if (symbolProfile.countries.length > 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(); - } - - if (country.code === 'JP') { - marketsAdvanced.japan = new Big(marketsAdvanced.japan) - .plus(country.weight) - .toNumber(); - } else if (country.code === 'CA' || country.code === 'US') { - marketsAdvanced.northAmerica = new Big(marketsAdvanced.northAmerica) - .plus(country.weight) - .toNumber(); - } else if (asiaPacificMarkets.includes(country.code)) { - marketsAdvanced.asiaPacific = new Big(marketsAdvanced.asiaPacific) - .plus(country.weight) - .toNumber(); - } else if (emergingMarkets.includes(country.code)) { - marketsAdvanced.emergingMarkets = new Big( - marketsAdvanced.emergingMarkets - ) - .plus(country.weight) - .toNumber(); - } else if (europeMarkets.includes(country.code)) { - marketsAdvanced.europe = new Big(marketsAdvanced.europe) - .plus(country.weight) - .toNumber(); - } else { - marketsAdvanced.otherMarkets = new Big(marketsAdvanced.otherMarkets) - .plus(country.weight) - .toNumber(); - } - } - } else { - markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY]) - .plus(valueInBaseCurrency) - .toNumber(); - - marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY]) - .plus(valueInBaseCurrency) - .toNumber(); + if (withMarkets) { + ({ markets, marketsAdvanced } = this.getMarkets({ + assetProfile, + valueInBaseCurrency + })); } holdings[symbol] = { @@ -568,10 +511,10 @@ export class PortfolioService { allocationInPercentage: filteredValueInBaseCurrency.eq(0) ? 0 : valueInBaseCurrency.div(filteredValueInBaseCurrency).toNumber(), - assetClass: symbolProfile.assetClass, - assetSubClass: symbolProfile.assetSubClass, - countries: symbolProfile.countries, - dataSource: symbolProfile.dataSource, + assetClass: assetProfile.assetClass, + assetSubClass: assetProfile.assetSubClass, + countries: assetProfile.countries, + dataSource: assetProfile.dataSource, dateOfFirstActivity: parseDate(firstBuyDate), dividend: dividend?.toNumber() ?? 0, grossPerformance: grossPerformance?.toNumber() ?? 0, @@ -582,7 +525,7 @@ export class PortfolioService { grossPerformanceWithCurrencyEffect?.toNumber() ?? 0, investment: investment.toNumber(), marketState: dataProviderResponse?.marketState ?? 'delayed', - name: symbolProfile.name, + name: assetProfile.name, netPerformance: netPerformance?.toNumber() ?? 0, netPerformancePercent: netPerformancePercentage?.toNumber() ?? 0, netPerformancePercentWithCurrencyEffect: @@ -590,8 +533,8 @@ export class PortfolioService { netPerformanceWithCurrencyEffect: netPerformanceWithCurrencyEffect?.toNumber() ?? 0, quantity: quantity.toNumber(), - sectors: symbolProfile.sectors, - url: symbolProfile.url, + sectors: assetProfile.sectors, + url: assetProfile.url, valueInBaseCurrency: valueInBaseCurrency.toNumber() }; } @@ -1630,6 +1573,86 @@ export class PortfolioService { }; } + private getMarkets({ + assetProfile, + valueInBaseCurrency + }: { + assetProfile: EnhancedSymbolProfile; + valueInBaseCurrency: Big; + }) { + const markets = { + [UNKNOWN_KEY]: 0, + developedMarkets: 0, + emergingMarkets: 0, + otherMarkets: 0 + }; + const marketsAdvanced = { + [UNKNOWN_KEY]: 0, + asiaPacific: 0, + emergingMarkets: 0, + europe: 0, + japan: 0, + northAmerica: 0, + otherMarkets: 0 + }; + + if (assetProfile.countries.length > 0) { + for (const country of assetProfile.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(); + } + + if (country.code === 'JP') { + marketsAdvanced.japan = new Big(marketsAdvanced.japan) + .plus(country.weight) + .toNumber(); + } else if (country.code === 'CA' || country.code === 'US') { + marketsAdvanced.northAmerica = new Big(marketsAdvanced.northAmerica) + .plus(country.weight) + .toNumber(); + } else if (asiaPacificMarkets.includes(country.code)) { + marketsAdvanced.asiaPacific = new Big(marketsAdvanced.asiaPacific) + .plus(country.weight) + .toNumber(); + } else if (emergingMarkets.includes(country.code)) { + marketsAdvanced.emergingMarkets = new Big( + marketsAdvanced.emergingMarkets + ) + .plus(country.weight) + .toNumber(); + } else if (europeMarkets.includes(country.code)) { + marketsAdvanced.europe = new Big(marketsAdvanced.europe) + .plus(country.weight) + .toNumber(); + } else { + marketsAdvanced.otherMarkets = new Big(marketsAdvanced.otherMarkets) + .plus(country.weight) + .toNumber(); + } + } + } else { + markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY]) + .plus(valueInBaseCurrency) + .toNumber(); + + marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY]) + .plus(valueInBaseCurrency) + .toNumber(); + } + + return { markets, marketsAdvanced }; + } + private getStreaks({ investments, savingsRate 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 67ad8231..0dba81d1 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 @@ -205,7 +205,8 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { private fetchPortfolioDetails() { return this.dataService.fetchPortfolioDetails({ - filters: this.userService.getFilters() + filters: this.userService.getFilters(), + withMarkets: true }); } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 088512ce..aeeb2f07 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -411,10 +411,12 @@ export class DataService { public fetchPortfolioDetails({ filters, - withLiabilities = false + withLiabilities = false, + withMarkets = false }: { filters?: Filter[]; withLiabilities?: boolean; + withMarkets?: boolean; } = {}): Observable { let params = this.buildFiltersAsQueryParams({ filters }); @@ -422,6 +424,10 @@ export class DataService { params = params.append('withLiabilities', withLiabilities); } + if (withMarkets) { + params = params.append('withMarkets', withMarkets); + } + return this.http .get('/api/v1/portfolio/details', { params