Browse Source

Implement time weighted portfolio performance calculation

pull/2778/head
Reto Kaul 2 years ago
committed by Thomas Kaul
parent
commit
7b9ebf8704
  1. 120
      apps/api/src/app/portfolio/portfolio-calculator.ts

120
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -17,6 +17,7 @@ import {
addYears, addYears,
endOfDay, endOfDay,
format, format,
intervalToDuration,
isAfter, isAfter,
isBefore, isBefore,
isSameDay, isSameDay,
@ -43,7 +44,7 @@ import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.in
import { TransactionPoint } from './interfaces/transaction-point.interface'; import { TransactionPoint } from './interfaces/transaction-point.interface';
export class PortfolioCalculator { export class PortfolioCalculator {
private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT = private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_TIME_WEIGHTED_INVESTMENT =
true; true;
private static readonly ENABLE_LOGGING = false; private static readonly ENABLE_LOGGING = false;
@ -238,12 +239,13 @@ export class PortfolioCalculator {
} }
} }
const valuesByDate: { const accumulatedValuesByDate: {
[date: string]: { [date: string]: {
maxTotalInvestmentValue: Big; maxTotalInvestmentValue: Big;
totalCurrentValue: Big; totalCurrentValue: Big;
totalInvestmentValue: Big; totalInvestmentValue: Big;
totalNetPerformanceValue: Big; totalNetPerformanceValue: Big;
totalTimeWeightedInvestmentValue: Big;
}; };
} = {}; } = {};
@ -253,6 +255,7 @@ export class PortfolioCalculator {
investmentValues: { [date: string]: Big }; investmentValues: { [date: string]: Big };
maxInvestmentValues: { [date: string]: Big }; maxInvestmentValues: { [date: string]: Big };
netPerformanceValues: { [date: string]: Big }; netPerformanceValues: { [date: string]: Big };
timeWeightedInvestmentValues: { [date: string]: Big };
}; };
} = {}; } = {};
@ -261,7 +264,8 @@ export class PortfolioCalculator {
currentValues, currentValues,
investmentValues, investmentValues,
maxInvestmentValues, maxInvestmentValues,
netPerformanceValues netPerformanceValues,
timeWeightedInvestmentValues
} = this.getSymbolMetrics({ } = this.getSymbolMetrics({
end, end,
marketSymbolMap, marketSymbolMap,
@ -275,7 +279,8 @@ export class PortfolioCalculator {
currentValues, currentValues,
investmentValues, investmentValues,
maxInvestmentValues, maxInvestmentValues,
netPerformanceValues netPerformanceValues,
timeWeightedInvestmentValues
}; };
} }
@ -293,38 +298,50 @@ export class PortfolioCalculator {
symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0); symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0);
const netPerformanceValue = const netPerformanceValue =
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0); symbolValues.netPerformanceValues?.[dateString] ?? new Big(0);
const timeWeightedInvestmentValue =
symbolValues.timeWeightedInvestmentValues?.[dateString] ?? new Big(0);
valuesByDate[dateString] = { accumulatedValuesByDate[dateString] = {
totalCurrentValue: ( totalCurrentValue: (
valuesByDate[dateString]?.totalCurrentValue ?? new Big(0) accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
).add(currentValue), ).add(currentValue),
totalInvestmentValue: ( totalInvestmentValue: (
valuesByDate[dateString]?.totalInvestmentValue ?? new Big(0) accumulatedValuesByDate[dateString]?.totalInvestmentValue ??
new Big(0)
).add(investmentValue), ).add(investmentValue),
totalTimeWeightedInvestmentValue: (
accumulatedValuesByDate[dateString]
?.totalTimeWeightedInvestmentValue ?? new Big(0)
).add(timeWeightedInvestmentValue),
maxTotalInvestmentValue: ( maxTotalInvestmentValue: (
valuesByDate[dateString]?.maxTotalInvestmentValue ?? new Big(0) accumulatedValuesByDate[dateString]?.maxTotalInvestmentValue ??
new Big(0)
).add(maxInvestmentValue), ).add(maxInvestmentValue),
totalNetPerformanceValue: ( totalNetPerformanceValue: (
valuesByDate[dateString]?.totalNetPerformanceValue ?? new Big(0) accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ??
new Big(0)
).add(netPerformanceValue) ).add(netPerformanceValue)
}; };
} }
} }
return Object.entries(valuesByDate).map(([date, values]) => { return Object.entries(accumulatedValuesByDate).map(([date, values]) => {
const { const {
maxTotalInvestmentValue, maxTotalInvestmentValue,
totalCurrentValue, totalCurrentValue,
totalInvestmentValue, totalInvestmentValue,
totalNetPerformanceValue totalNetPerformanceValue,
totalTimeWeightedInvestmentValue
} = values; } = values;
const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0) let investmentValue =
PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_TIME_WEIGHTED_INVESTMENT
? totalTimeWeightedInvestmentValue
: maxTotalInvestmentValue;
const netPerformanceInPercentage = investmentValue.eq(0)
? 0 ? 0
: totalNetPerformanceValue : totalNetPerformanceValue.div(investmentValue).mul(100).toNumber();
.div(maxTotalInvestmentValue)
.mul(100)
.toNumber();
return { return {
date, date,
@ -1018,6 +1035,7 @@ export class PortfolioCalculator {
let averagePriceAtEndDate = new Big(0); let averagePriceAtEndDate = new Big(0);
let averagePriceAtStartDate = new Big(0); let averagePriceAtStartDate = new Big(0);
const currentValues: { [date: string]: Big } = {};
let feesAtStartDate = new Big(0); let feesAtStartDate = new Big(0);
let fees = new Big(0); let fees = new Big(0);
let grossPerformance = new Big(0); let grossPerformance = new Big(0);
@ -1025,12 +1043,12 @@ export class PortfolioCalculator {
let grossPerformanceFromSells = new Big(0); let grossPerformanceFromSells = new Big(0);
let initialValue: Big; let initialValue: Big;
let investmentAtStartDate: Big; let investmentAtStartDate: Big;
const currentValues: { [date: string]: Big } = {};
const investmentValues: { [date: string]: Big } = {}; const investmentValues: { [date: string]: Big } = {};
const maxInvestmentValues: { [date: string]: Big } = {}; const maxInvestmentValues: { [date: string]: Big } = {};
let lastAveragePrice = new Big(0); let lastAveragePrice = new Big(0);
let maxTotalInvestment = new Big(0); let maxTotalInvestment = new Big(0);
const netPerformanceValues: { [date: string]: Big } = {}; const netPerformanceValues: { [date: string]: Big } = {};
const timeWeightedInvestmentValues: { [date: string]: Big } = {};
let totalInvestment = new Big(0); let totalInvestment = new Big(0);
let totalInvestmentWithGrossPerformanceFromSell = new Big(0); let totalInvestmentWithGrossPerformanceFromSell = new Big(0);
let totalUnits = new Big(0); let totalUnits = new Big(0);
@ -1122,6 +1140,9 @@ export class PortfolioCalculator {
return order.itemType === 'end'; return order.itemType === 'end';
}); });
let totalInvestmentDays = 0;
let sumOfTimeWeightedInvestments = new Big(0);
for (let i = 0; i < orders.length; i += 1) { for (let i = 0; i < orders.length; i += 1) {
const order = orders[i]; const order = orders[i];
@ -1243,7 +1264,25 @@ export class PortfolioCalculator {
grossPerformanceAtStartDate = grossPerformance; grossPerformanceAtStartDate = grossPerformance;
} }
if (isChartMode && i > indexOfStartOrder) { if (i > indexOfStartOrder) {
// Calculate the number of days since the previous order
const orderDate = new Date(order.date);
const previousOrderDate = new Date(orders[i - 1].date);
const daysSinceLastOrder = intervalToDuration({
end: orderDate,
start: previousOrderDate
}).days;
// Sum up the total investment days since the start date to calculate the
// time weighted investment
totalInvestmentDays += daysSinceLastOrder;
sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add(
totalInvestment.mul(daysSinceLastOrder)
);
if (isChartMode) {
currentValues[order.date] = valueOfInvestment; currentValues[order.date] = valueOfInvestment;
netPerformanceValues[order.date] = grossPerformance netPerformanceValues[order.date] = grossPerformance
.minus(grossPerformanceAtStartDate) .minus(grossPerformanceAtStartDate)
@ -1251,6 +1290,12 @@ export class PortfolioCalculator {
investmentValues[order.date] = totalInvestment; investmentValues[order.date] = totalInvestment;
maxInvestmentValues[order.date] = maxTotalInvestment; maxInvestmentValues[order.date] = maxTotalInvestment;
timeWeightedInvestmentValues[order.date] =
totalInvestmentDays > 0
? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
: new Big(0);
}
} }
if (PortfolioCalculator.ENABLE_LOGGING) { if (PortfolioCalculator.ENABLE_LOGGING) {
@ -1274,12 +1319,28 @@ export class PortfolioCalculator {
.minus(grossPerformanceAtStartDate) .minus(grossPerformanceAtStartDate)
.minus(fees.minus(feesAtStartDate)); .minus(fees.minus(feesAtStartDate));
const timeWeightedAverageInvestmentBetweenStartAndEndDate =
totalInvestmentDays > 0
? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
: new Big(0);
const maxInvestmentBetweenStartAndEndDate = valueAtStartDate.plus( const maxInvestmentBetweenStartAndEndDate = valueAtStartDate.plus(
maxTotalInvestment.minus(investmentAtStartDate) maxTotalInvestment.minus(investmentAtStartDate)
); );
const grossPerformancePercentage = let grossPerformancePercentage: Big;
PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT ||
if (
PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_TIME_WEIGHTED_INVESTMENT
) {
grossPerformancePercentage =
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0)
? totalGrossPerformance.div(
timeWeightedAverageInvestmentBetweenStartAndEndDate
)
: new Big(0);
} else {
grossPerformancePercentage =
averagePriceAtStartDate.eq(0) || averagePriceAtStartDate.eq(0) ||
averagePriceAtEndDate.eq(0) || averagePriceAtEndDate.eq(0) ||
orders[indexOfStartOrder].unitPrice.eq(0) orders[indexOfStartOrder].unitPrice.eq(0)
@ -1295,13 +1356,25 @@ export class PortfolioCalculator {
orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate) orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
) )
.minus(1); .minus(1);
}
const feesPerUnit = totalUnits.gt(0) const feesPerUnit = totalUnits.gt(0)
? fees.minus(feesAtStartDate).div(totalUnits) ? fees.minus(feesAtStartDate).div(totalUnits)
: new Big(0); : new Big(0);
const netPerformancePercentage = let netPerformancePercentage: Big;
PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT ||
if (
PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_TIME_WEIGHTED_INVESTMENT
) {
netPerformancePercentage =
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0)
? totalNetPerformance.div(
timeWeightedAverageInvestmentBetweenStartAndEndDate
)
: new Big(0);
} else {
netPerformancePercentage =
averagePriceAtStartDate.eq(0) || averagePriceAtStartDate.eq(0) ||
averagePriceAtEndDate.eq(0) || averagePriceAtEndDate.eq(0) ||
orders[indexOfStartOrder].unitPrice.eq(0) orders[indexOfStartOrder].unitPrice.eq(0)
@ -1318,6 +1391,7 @@ export class PortfolioCalculator {
orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate) orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
) )
.minus(1); .minus(1);
}
if (PortfolioCalculator.ENABLE_LOGGING) { if (PortfolioCalculator.ENABLE_LOGGING) {
console.log( console.log(
@ -1330,6 +1404,7 @@ export class PortfolioCalculator {
2 2
)} -> ${averagePriceAtEndDate.toFixed(2)} )} -> ${averagePriceAtEndDate.toFixed(2)}
Total investment: ${totalInvestment.toFixed(2)} Total investment: ${totalInvestment.toFixed(2)}
Time weighted investment: ${timeWeightedAverageInvestmentBetweenStartAndEndDate.toFixed()}
Max. total investment: ${maxTotalInvestment.toFixed(2)} Max. total investment: ${maxTotalInvestment.toFixed(2)}
Gross performance: ${totalGrossPerformance.toFixed( Gross performance: ${totalGrossPerformance.toFixed(
2 2
@ -1349,6 +1424,7 @@ export class PortfolioCalculator {
maxInvestmentValues, maxInvestmentValues,
netPerformancePercentage, netPerformancePercentage,
netPerformanceValues, netPerformanceValues,
timeWeightedInvestmentValues,
grossPerformance: totalGrossPerformance, grossPerformance: totalGrossPerformance,
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
netPerformance: totalNetPerformance netPerformance: totalNetPerformance

Loading…
Cancel
Save