Browse Source

Merge pull request #73 from dandevaud/feature/Performance-Enhancements

Fix TimeWeighted performance and readd multi select fitlters
pull/5027/head
dandevaud 1 year ago
committed by GitHub
parent
commit
57740e5229
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      apps/api/src/app/portfolio/portfolio-calculator-novn-baln-buy-and-sell.spec.ts
  2. 291
      apps/api/src/app/portfolio/portfolio-calculator.ts
  3. 4
      apps/client/src/app/components/header/header.component.ts
  4. 6
      apps/client/src/app/services/user/user.service.ts
  5. 3
      libs/common/src/lib/interfaces/symbol-metrics.interface.ts
  6. 51
      libs/ui/src/lib/assistant/assistant.component.ts
  7. 6
      libs/ui/src/lib/assistant/assistant.html

2
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,

291
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<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let feesAtStartDate: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let grossPerformance: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let grossPerformanceAtStartDate: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let grossPerformanceFromSells: WithCurrencyEffect<Big> = {
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<Big> = {
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<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let totalInvestmentWithGrossPerformanceFromSell: WithCurrencyEffect<Big> = {
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<Big>;
investmentAtStartDate: any;
valueAtStartDate: WithCurrencyEffect<Big>;
maxTotalInvestment: Big;
averagePriceAtEndDate: Big;
initialValue: any;
fees: WithCurrencyEffect<Big>;
feesAtStartDate: WithCurrencyEffect<Big>;
lastAveragePrice: WithCurrencyEffect<Big>;
grossPerformanceFromSells: WithCurrencyEffect<Big>;
totalInvestmentWithGrossPerformanceFromSell: WithCurrencyEffect<Big>;
grossPerformance: WithCurrencyEffect<Big>;
grossPerformanceAtStartDate: WithCurrencyEffect<Big>;
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<Big> = {
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<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let feesAtStartDate: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let grossPerformance: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let grossPerformanceAtStartDate: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let grossPerformanceFromSells: WithCurrencyEffect<Big> = {
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<Big> = {
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<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let totalInvestmentWithGrossPerformanceFromSell: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let totalUnits = new Big(0);
let valueAtStartDate: WithCurrencyEffect<Big> = {
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
};
}

4
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

6
apps/client/src/app/services/user/user.service.ts

@ -53,21 +53,21 @@ export class UserService extends ObservableStore<UserStoreState> {
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'
});
}

3
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;
};
}

51
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<string>(undefined),
assetClass: new FormControl<string>(undefined),
tag: new FormControl<string>(undefined)
account: new FormControl<string[]>(undefined),
assetClass: new FormControl<string[]>(undefined),
tag: new FormControl<string[]>(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();
}

6
libs/ui/src/lib/assistant/assistant.html

@ -105,7 +105,7 @@
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Accounts</mat-label>
<mat-select formControlName="account">
<mat-select formControlName="account" multiple>
<mat-option [value]="null" />
@for (account of accounts; track account.id) {
<mat-option [value]="account.id">
@ -125,7 +125,7 @@
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Tags</mat-label>
<mat-select formControlName="tag">
<mat-select formControlName="tag" multiple>
<mat-option [value]="null" />
@for (tag of tags; track tag.id) {
<mat-option [value]="tag.id">{{ tag.label }}</mat-option>
@ -136,7 +136,7 @@
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Asset Classes</mat-label>
<mat-select formControlName="assetClass">
<mat-select formControlName="assetClass" multiple>
<mat-option [value]="null" />
@for (assetClass of assetClasses; track assetClass.id) {
<mat-option [value]="assetClass.id"

Loading…
Cancel
Save