diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-baln-buy-and-sell.spec.ts index 46da916c5..fe96d90a7 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-novn-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-baln-buy-and-sell.spec.ts @@ -141,7 +141,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentage: 13.100263852242744, netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744, netPerformanceWithCurrencyEffect: 19.86, - timeWeightedPerformance: 13.100263852242744, + timeWeightedPerformance: 0, totalInvestment: 0, totalInvestmentValueWithCurrencyEffect: 0, value: 0, diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 00b0b522b..1b158dec7 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -317,6 +317,7 @@ export class PortfolioCalculator { timeWeightedInvestmentValues: { [date: string]: Big }; timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big }; netPerformanceValuesPercentage: { [date: string]: Big }; + unitPrices: { [date: string]: Big }; }; } = {}; @@ -367,6 +368,7 @@ export class PortfolioCalculator { timeWeightedInvestmentValues: { [date: string]: Big }; timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big }; netPerformanceValuesPercentage: { [date: string]: Big }; + unitPrices: { [date: string]: Big }; }; }, calculateTimeWeightedPerformance: boolean, @@ -390,13 +392,11 @@ export class PortfolioCalculator { ? format(previousDate, DATE_FORMAT) : null; let totalCurrentValue = new Big(0); - let maxTotalInvestmentValue = new Big(0); let previousTotalInvestmentValue = new Big(0); - let timeWeightedPerformance = new Big(0); if (calculateTimeWeightedPerformance && previousDateString) { previousTotalInvestmentValue = - accumulatedValuesByDate[previousDateString].totalInvestmentValue; + accumulatedValuesByDate[previousDateString].totalCurrentValue; } for (const symbol of Object.keys(valuesBySymbol)) { @@ -406,6 +406,8 @@ export class PortfolioCalculator { totalCurrentValue = totalCurrentValue.plus(symbolCurrentValues); + let timeWeightedPerformanceContribution = new Big(0); + if ( previousTotalInvestmentValue.toNumber() && symbolValues.netPerformanceValuesPercentage && @@ -416,28 +418,22 @@ export class PortfolioCalculator { const previousValue = symbolValues.currentValues?.[previousDateString] ?? new Big(0); const netPerformance = - symbolValues.netPerformanceValuesPercentage?.[dateString] ?? - new Big(0); - const timeWeightedPerformanceContribution = previousValue + symbolValues.unitPrices?.[dateString] && + symbolValues.unitPrices?.[previousDateString] + ? symbolValues.unitPrices[dateString] + .div(symbolValues.unitPrices[previousDateString]) + .minus(1) + : new Big(0); + timeWeightedPerformanceContribution = previousValue .div(previousTotalInvestmentValue) - .mul(netPerformance) - .mul(100); - timeWeightedPerformance = timeWeightedPerformance.plus( - timeWeightedPerformanceContribution - ); + .mul(netPerformance); } - - let totalTimeWeightedPerformance = timeWeightedPerformance.plus( - accumulatedValuesByDate[previousDateString] - ?.totalTimeWeightedPerformance ?? new Big(0) - ); - accumulatedValuesByDate = this.accumulatedValuesByDate( valuesBySymbol, symbol, dateString, accumulatedValuesByDate, - totalTimeWeightedPerformance + timeWeightedPerformanceContribution ); } @@ -453,6 +449,19 @@ export class PortfolioCalculator { totalNetPerformanceValue } = accumulatedValuesByDate[dateString]; + let totalNetTimeWeightedPerformance = new Big(0); + + if (previousDateString) { + totalNetTimeWeightedPerformance = ( + accumulatedValuesByDate[previousDateString] + ?.totalTimeWeightedPerformance ?? new Big(0) + ) + .plus(1) + .mul(totalTimeWeightedPerformance.plus(1)) + .minus(1) + .mul(100); + } + const netPerformanceInPercentage = totalTimeWeightedInvestmentValue.eq(0) ? 0 : totalNetPerformanceValue @@ -476,7 +485,7 @@ export class PortfolioCalculator { totalInvestment: totalInvestmentValue.toNumber(), value: totalCurrentValue.toNumber(), valueWithCurrencyEffect: totalCurrentValueWithCurrencyEffect.toNumber(), - timeWeightedPerformance: totalTimeWeightedPerformance.toNumber(), + timeWeightedPerformance: totalNetTimeWeightedPerformance.toNumber(), investmentValueWithCurrencyEffect: investmentValueWithCurrencyEffect.toNumber(), netPerformanceWithCurrencyEffect: @@ -589,7 +598,10 @@ export class PortfolioCalculator { accumulatedValuesByDate[dateString] ?.totalTimeWeightedInvestmentValueWithCurrencyEffect ?? new Big(0) ).add(timeWeightedInvestmentValueWithCurrencyEffect), - totalTimeWeightedPerformance: timeWeightedPerformance + totalTimeWeightedPerformance: ( + accumulatedValuesByDate[dateString]?.totalTimeWeightedPerformance ?? + new Big(0) + ).add(timeWeightedPerformance) }; return accumulatedValuesByDate; @@ -615,6 +627,7 @@ export class PortfolioCalculator { timeWeightedInvestmentValues: { [date: string]: Big }; timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big }; netPerformanceValuesPercentage: { [date: string]: Big }; + unitPrices: { [date: string]: Big }; }; }, currencies: { [symbol: string]: string } @@ -630,7 +643,8 @@ export class PortfolioCalculator { netPerformanceValuesWithCurrencyEffect, timeWeightedInvestmentValues, timeWeightedInvestmentValuesWithCurrencyEffect, - netPerformanceValuesPercentage + netPerformanceValuesPercentage, + unitPrices } = this.getSymbolMetrics({ end, marketSymbolMap, @@ -652,7 +666,8 @@ export class PortfolioCalculator { netPerformanceValuesWithCurrencyEffect, timeWeightedInvestmentValues, timeWeightedInvestmentValuesWithCurrencyEffect, - netPerformanceValuesPercentage + netPerformanceValuesPercentage, + unitPrices }; } } @@ -1162,78 +1177,55 @@ export class PortfolioCalculator { symbol: string; calculatePerformance?: boolean; }): SymbolMetrics { - const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; - const currentValues: WithCurrencyEffect<{ [date: string]: Big }> = { - Value: {}, - WithCurrencyEffect: {} - }; - let fees: WithCurrencyEffect = { - Value: new Big(0), - WithCurrencyEffect: new Big(0) - }; - let feesAtStartDate: WithCurrencyEffect = { - Value: new Big(0), - WithCurrencyEffect: new Big(0) - }; - let grossPerformance: WithCurrencyEffect = { - Value: new Big(0), - WithCurrencyEffect: new Big(0) - }; - let grossPerformanceAtStartDate: WithCurrencyEffect = { - Value: new Big(0), - WithCurrencyEffect: new Big(0) - }; - let grossPerformanceFromSells: WithCurrencyEffect = { - Value: new Big(0), - WithCurrencyEffect: new Big(0) - }; - let averagePriceAtEndDate = new Big(0); - let averagePriceAtStartDate = new Big(0); - const investmentValues: WithCurrencyEffect<{ [date: string]: Big }> = { - Value: {}, - WithCurrencyEffect: {} - }; - const maxInvestmentValues: { [date: string]: Big } = {}; - let maxTotalInvestment = new Big(0); - const netPerformanceValuesPercentage: { [date: string]: Big } = {}; - let initialValue; - let investmentAtStartDate; - const investmentValuesAccumulated: WithCurrencyEffect<{ - [date: string]: Big; - }> = { - Value: {}, - WithCurrencyEffect: {} - }; - let lastAveragePrice: WithCurrencyEffect = { - Value: new Big(0), - WithCurrencyEffect: new Big(0) - }; - const netPerformanceValues: WithCurrencyEffect<{ [date: string]: Big }> = { - Value: {}, - WithCurrencyEffect: {} - }; - const timeWeightedInvestmentValues: WithCurrencyEffect<{ - [date: string]: Big; - }> = { - Value: {}, - WithCurrencyEffect: {} - }; - - let totalInvestment: WithCurrencyEffect = { - Value: new Big(0), - WithCurrencyEffect: new Big(0) - }; - - let totalInvestmentWithGrossPerformanceFromSell: WithCurrencyEffect = { - Value: new Big(0), - WithCurrencyEffect: new Big(0) - }; + let { + averagePriceAtStartDate, + totalUnits, + totalInvestment, + investmentAtStartDate, + valueAtStartDate, + maxTotalInvestment, + averagePriceAtEndDate, + initialValue, + fees, + feesAtStartDate, + lastAveragePrice, + grossPerformanceFromSells, + totalInvestmentWithGrossPerformanceFromSell, + grossPerformance, + grossPerformanceAtStartDate, + currentValues, + netPerformanceValues, + netPerformanceValuesPercentage, + investmentValues, + investmentValuesAccumulated, + maxInvestmentValues, + timeWeightedInvestmentValues + }: { + averagePriceAtStartDate: Big; + totalUnits: Big; + totalInvestment: WithCurrencyEffect; + investmentAtStartDate: any; + valueAtStartDate: WithCurrencyEffect; + maxTotalInvestment: Big; + averagePriceAtEndDate: Big; + initialValue: any; + fees: WithCurrencyEffect; + feesAtStartDate: WithCurrencyEffect; + lastAveragePrice: WithCurrencyEffect; + grossPerformanceFromSells: WithCurrencyEffect; + totalInvestmentWithGrossPerformanceFromSell: WithCurrencyEffect; + grossPerformance: WithCurrencyEffect; + grossPerformanceAtStartDate: WithCurrencyEffect; + currentValues: WithCurrencyEffect<{ [date: string]: Big }>; + netPerformanceValues: WithCurrencyEffect<{ [date: string]: Big }>; + netPerformanceValuesPercentage: { [date: string]: Big }; + investmentValues: WithCurrencyEffect<{ [date: string]: Big }>; + investmentValuesAccumulated: WithCurrencyEffect<{ [date: string]: Big }>; + maxInvestmentValues: { [date: string]: Big }; + timeWeightedInvestmentValues: WithCurrencyEffect<{ [date: string]: Big }>; + } = this.InitializeSymbolMetricValues(); - let totalUnits = new Big(0); - let valueAtStartDate: WithCurrencyEffect = { - Value: new Big(0), - WithCurrencyEffect: new Big(0) - }; + const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; // Clone orders to keep the original values in this.orders let orders: PortfolioOrderItem[] = cloneDeep(this.orders).filter( @@ -1268,7 +1260,8 @@ export class PortfolioCalculator { timeWeightedInvestmentValuesWithCurrencyEffect: {}, totalInvestment: new Big(0), totalInvestmentWithCurrencyEffect: new Big(0), - netPerformanceValuesPercentage: {} + netPerformanceValuesPercentage: {}, + unitPrices: {} }; } @@ -1309,7 +1302,8 @@ export class PortfolioCalculator { timeWeightedInvestmentWithCurrencyEffect: new Big(0), totalInvestment: new Big(0), totalInvestmentWithCurrencyEffect: new Big(0), - netPerformanceValuesPercentage: {} + netPerformanceValuesPercentage: {}, + unitPrices: {} }; } @@ -1385,6 +1379,12 @@ export class PortfolioCalculator { calculatePerformance ); + let unitPrices = Object.keys(marketSymbolMap).reduce( + (obj, date) => + (obj = Object.assign(obj, { [date]: marketSymbolMap[date][symbol] })), + {} + ); + return { currentValues: result.currentValues.Value, currentValuesWithCurrencyEffect: result.currentValues.WithCurrencyEffect, @@ -1422,7 +1422,106 @@ export class PortfolioCalculator { result.timeWeightedAverageInvestmentBetweenStartAndEndDate.Value, timeWeightedInvestmentWithCurrencyEffect: result.timeWeightedAverageInvestmentBetweenStartAndEndDate - .WithCurrencyEffect + .WithCurrencyEffect, + unitPrices + }; + } + + private InitializeSymbolMetricValues() { + const currentValues: WithCurrencyEffect<{ [date: string]: Big }> = { + Value: {}, + WithCurrencyEffect: {} + }; + let fees: WithCurrencyEffect = { + Value: new Big(0), + WithCurrencyEffect: new Big(0) + }; + let feesAtStartDate: WithCurrencyEffect = { + Value: new Big(0), + WithCurrencyEffect: new Big(0) + }; + let grossPerformance: WithCurrencyEffect = { + Value: new Big(0), + WithCurrencyEffect: new Big(0) + }; + let grossPerformanceAtStartDate: WithCurrencyEffect = { + Value: new Big(0), + WithCurrencyEffect: new Big(0) + }; + let grossPerformanceFromSells: WithCurrencyEffect = { + Value: new Big(0), + WithCurrencyEffect: new Big(0) + }; + let averagePriceAtEndDate = new Big(0); + let averagePriceAtStartDate = new Big(0); + const investmentValues: WithCurrencyEffect<{ [date: string]: Big }> = { + Value: {}, + WithCurrencyEffect: {} + }; + const maxInvestmentValues: { [date: string]: Big } = {}; + let maxTotalInvestment = new Big(0); + const netPerformanceValuesPercentage: { [date: string]: Big } = {}; + let initialValue; + let investmentAtStartDate; + const investmentValuesAccumulated: WithCurrencyEffect<{ + [date: string]: Big; + }> = { + Value: {}, + WithCurrencyEffect: {} + }; + let lastAveragePrice: WithCurrencyEffect = { + Value: new Big(0), + WithCurrencyEffect: new Big(0) + }; + const netPerformanceValues: WithCurrencyEffect<{ [date: string]: Big }> = { + Value: {}, + WithCurrencyEffect: {} + }; + const timeWeightedInvestmentValues: WithCurrencyEffect<{ + [date: string]: Big; + }> = { + Value: {}, + WithCurrencyEffect: {} + }; + + let totalInvestment: WithCurrencyEffect = { + Value: new Big(0), + WithCurrencyEffect: new Big(0) + }; + + let totalInvestmentWithGrossPerformanceFromSell: WithCurrencyEffect = { + Value: new Big(0), + WithCurrencyEffect: new Big(0) + }; + + let totalUnits = new Big(0); + let valueAtStartDate: WithCurrencyEffect = { + Value: new Big(0), + WithCurrencyEffect: new Big(0) + }; + return { + averagePriceAtStartDate, + totalUnits, + totalInvestment, + investmentAtStartDate, + valueAtStartDate, + maxTotalInvestment, + averagePriceAtEndDate, + initialValue, + fees, + feesAtStartDate, + lastAveragePrice, + grossPerformanceFromSells, + totalInvestmentWithGrossPerformanceFromSell, + grossPerformance, + grossPerformanceAtStartDate, + currentValues, + netPerformanceValues, + netPerformanceValuesPercentage, + investmentValues, + investmentValuesAccumulated, + maxInvestmentValues, + timeWeightedInvestmentValues }; } diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts index 0e2cd2aa7..377b429bc 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -179,7 +179,9 @@ export class HeaderComponent implements OnChanges { filtersType = 'tags'; } - userSetting[`filters.${filtersType}`] = filter.id ? [filter.id] : null; + userSetting[`filters.${filtersType}`] = filters + .filter((f) => f.type === filter.type) + .map((f) => f.id); } this.dataService diff --git a/apps/client/src/app/services/user/user.service.ts b/apps/client/src/app/services/user/user.service.ts index d8cb63d0b..d15cbdb4d 100644 --- a/apps/client/src/app/services/user/user.service.ts +++ b/apps/client/src/app/services/user/user.service.ts @@ -53,21 +53,21 @@ export class UserService extends ObservableStore { if (user.settings['filters.accounts']) { filters.push({ - id: user.settings['filters.accounts'][0], + id: user.settings['filters.accounts'].join(','), type: 'ACCOUNT' }); } if (user.settings['filters.assetClasses']) { filters.push({ - id: user.settings['filters.assetClasses'][0], + id: user.settings['filters.assetClasses'].join(','), type: 'ASSET_CLASS' }); } if (user.settings['filters.tags']) { filters.push({ - id: user.settings['filters.tags'][0], + id: user.settings['filters.tags'].join(','), type: 'TAG' }); } diff --git a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts index 41f161513..c03af5e9e 100644 --- a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts +++ b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts @@ -44,4 +44,7 @@ export interface SymbolMetrics { timeWeightedInvestmentWithCurrencyEffect: Big; totalInvestment: Big; totalInvestmentWithCurrencyEffect: Big; + unitPrices: { + [date: string]: Big; + }; } diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts index f4f9beea1..8e8b903de 100644 --- a/libs/ui/src/lib/assistant/assistant.component.ts +++ b/libs/ui/src/lib/assistant/assistant.component.ts @@ -24,6 +24,7 @@ import { import { FormBuilder, FormControl } from '@angular/forms'; import { MatMenuTrigger } from '@angular/material/menu'; import { Account, AssetClass } from '@prisma/client'; +import { filter } from 'lodash'; import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs'; import { catchError, @@ -117,9 +118,9 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { { label: $localize`Max`, value: 'max' } ]; public filterForm = this.formBuilder.group({ - account: new FormControl(undefined), - assetClass: new FormControl(undefined), - tag: new FormControl(undefined) + account: new FormControl(undefined), + assetClass: new FormControl(undefined), + tag: new FormControl(undefined) }); public isLoading = false; public isOpen = false; @@ -203,9 +204,9 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { this.filterForm.setValue( { - account: this.user?.settings?.['filters.accounts']?.[0] ?? null, - assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null, - tag: this.user?.settings?.['filters.tags']?.[0] ?? null + account: this.user?.settings?.['filters.accounts'] ?? null, + assetClass: this.user?.settings?.['filters.assetClasses'] ?? null, + tag: this.user?.settings?.['filters.tags'] ?? null }, { emitEvent: false @@ -213,7 +214,7 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { ); } - public hasFilter(aFormValue: { [key: string]: string }) { + public hasFilter(aFormValue: { [key: string]: string[] }) { return Object.values(aFormValue).some((value) => { return !!value; }); @@ -243,20 +244,28 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit { } public onApplyFilters() { - this.filtersChanged.emit([ - { - id: this.filterForm.get('account').value, - type: 'ACCOUNT' - }, - { - id: this.filterForm.get('assetClass').value, - type: 'ASSET_CLASS' - }, - { - id: this.filterForm.get('tag').value, - type: 'TAG' - } - ]); + let accountFilters = + this.filterForm + .get('account') + .value?.reduce( + (arr, val) => [...arr, { id: val, type: 'ACCOUNT' }], + [] + ) ?? []; + let assetClassFilters = + this.filterForm + .get('assetClass') + .value?.reduce( + (arr, val) => [...arr, { id: val, type: 'ASSET_CLASS' }], + [] + ) ?? []; + let tagFilters = + this.filterForm + .get('tag') + .value?.reduce((arr, val) => [...arr, { id: val, type: 'TAG' }], []) ?? + []; + let filters = [...accountFilters, ...assetClassFilters]; + filters = [...filters, ...tagFilters]; + this.filtersChanged.emit(filters); this.onCloseAssistant(); } diff --git a/libs/ui/src/lib/assistant/assistant.html b/libs/ui/src/lib/assistant/assistant.html index 073833475..7359bb20e 100644 --- a/libs/ui/src/lib/assistant/assistant.html +++ b/libs/ui/src/lib/assistant/assistant.html @@ -105,7 +105,7 @@
Accounts - + @for (account of accounts; track account.id) { @@ -125,7 +125,7 @@
Tags - + @for (tag of tags; track tag.id) { {{ tag.label }} @@ -136,7 +136,7 @@
Asset Classes - + @for (assetClass of assetClasses; track assetClass.id) {