Improve calculation of overall performance percentage (#701)

* Improve calculation of overall performance percentage

Co-authored-by: Reto Kaul <retokaul@sublimd.com>
pull/717/head
gizmodus 3 years ago committed by GitHub
parent 2a2a5f4da5
commit a5771f601d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -33,6 +33,8 @@ import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.in
import { TransactionPoint } from './interfaces/transaction-point.interface'; import { TransactionPoint } from './interfaces/transaction-point.interface';
export class PortfolioCalculatorNew { export class PortfolioCalculatorNew {
private static readonly ENABLE_LOGGING = false;
private currency: string; private currency: string;
private currentRateService: CurrentRateService; private currentRateService: CurrentRateService;
private orders: PortfolioOrder[]; private orders: PortfolioOrder[];
@ -228,7 +230,7 @@ export class PortfolioCalculatorNew {
const initialValues: { [symbol: string]: Big } = {}; const initialValues: { [symbol: string]: Big } = {};
const positions: TimelinePosition[] = []; const positions: TimelinePosition[] = [];
let hasErrorsInSymbolMetrics = false; let hasAnySymbolMetricsErrors = false;
for (const item of lastTransactionPoint.items) { for (const item of lastTransactionPoint.items) {
const marketValue = marketSymbolMap[todayString]?.[item.symbol]; const marketValue = marketSymbolMap[todayString]?.[item.symbol];
@ -246,8 +248,7 @@ export class PortfolioCalculatorNew {
symbol: item.symbol symbol: item.symbol
}); });
hasErrorsInSymbolMetrics = hasErrorsInSymbolMetrics || hasErrors; hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
initialValues[item.symbol] = initialValue; initialValues[item.symbol] = initialValue;
positions.push({ positions.push({
@ -272,12 +273,13 @@ export class PortfolioCalculatorNew {
transactionCount: item.transactionCount transactionCount: item.transactionCount
}); });
} }
const overall = this.calculateOverallPerformance(positions, initialValues); const overall = this.calculateOverallPerformance(positions, initialValues);
return { return {
...overall, ...overall,
positions, positions,
hasErrors: hasErrorsInSymbolMetrics || overall.hasErrors hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
}; };
} }
@ -395,16 +397,16 @@ export class PortfolioCalculatorNew {
private calculateOverallPerformance( private calculateOverallPerformance(
positions: TimelinePosition[], positions: TimelinePosition[],
initialValues: { [p: string]: Big } initialValues: { [symbol: string]: Big }
) { ) {
let hasErrors = false;
let currentValue = new Big(0); let currentValue = new Big(0);
let totalInvestment = new Big(0);
let grossPerformance = new Big(0); let grossPerformance = new Big(0);
let grossPerformancePercentage = new Big(0); let grossPerformancePercentage = new Big(0);
let hasErrors = false;
let netPerformance = new Big(0); let netPerformance = new Big(0);
let netPerformancePercentage = new Big(0); let netPerformancePercentage = new Big(0);
let completeInitialValue = new Big(0); let sumOfWeights = new Big(0);
let totalInvestment = new Big(0);
for (const currentPosition of positions) { for (const currentPosition of positions) {
if (currentPosition.marketPrice) { if (currentPosition.marketPrice) {
@ -414,27 +416,34 @@ export class PortfolioCalculatorNew {
} else { } else {
hasErrors = true; hasErrors = true;
} }
totalInvestment = totalInvestment.plus(currentPosition.investment); totalInvestment = totalInvestment.plus(currentPosition.investment);
if (currentPosition.grossPerformance) { if (currentPosition.grossPerformance) {
grossPerformance = grossPerformance.plus( grossPerformance = grossPerformance.plus(
currentPosition.grossPerformance currentPosition.grossPerformance
); );
netPerformance = netPerformance.plus(currentPosition.netPerformance); netPerformance = netPerformance.plus(currentPosition.netPerformance);
} else if (!currentPosition.quantity.eq(0)) { } else if (!currentPosition.quantity.eq(0)) {
hasErrors = true; hasErrors = true;
} }
if ( if (currentPosition.grossPerformancePercentage) {
currentPosition.grossPerformancePercentage && // Use the average from the initial value and the current investment as
initialValues[currentPosition.symbol] // a weight
) { const weight = (initialValues[currentPosition.symbol] ?? new Big(0))
const currentInitialValue = initialValues[currentPosition.symbol]; .plus(currentPosition.investment)
completeInitialValue = completeInitialValue.plus(currentInitialValue); .div(2);
sumOfWeights = sumOfWeights.plus(weight);
grossPerformancePercentage = grossPerformancePercentage.plus( grossPerformancePercentage = grossPerformancePercentage.plus(
currentPosition.grossPerformancePercentage.mul(currentInitialValue) currentPosition.grossPerformancePercentage.mul(weight)
); );
netPerformancePercentage = netPerformancePercentage.plus( netPerformancePercentage = netPerformancePercentage.plus(
currentPosition.netPerformancePercentage.mul(currentInitialValue) currentPosition.netPerformancePercentage.mul(weight)
); );
} else if (!currentPosition.quantity.eq(0)) { } else if (!currentPosition.quantity.eq(0)) {
Logger.warn( Logger.warn(
@ -444,11 +453,12 @@ export class PortfolioCalculatorNew {
} }
} }
if (!completeInitialValue.eq(0)) { if (sumOfWeights.gt(0)) {
grossPerformancePercentage = grossPerformancePercentage = grossPerformancePercentage.div(sumOfWeights);
grossPerformancePercentage.div(completeInitialValue); netPerformancePercentage = netPerformancePercentage.div(sumOfWeights);
netPerformancePercentage = } else {
netPerformancePercentage.div(completeInitialValue); grossPerformancePercentage = new Big(0);
netPerformancePercentage = new Big(0);
} }
return { return {
@ -657,6 +667,8 @@ export class PortfolioCalculatorNew {
}; };
} }
let averagePriceAtEndDate = new Big(0);
let averagePriceAtStartDate = new Big(0);
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);
@ -666,17 +678,13 @@ export class PortfolioCalculatorNew {
let lastAveragePrice = new Big(0); let lastAveragePrice = new Big(0);
let lastTransactionInvestment = new Big(0); let lastTransactionInvestment = new Big(0);
let lastValueOfInvestmentBeforeTransaction = new Big(0); let lastValueOfInvestmentBeforeTransaction = new Big(0);
let maxTotalInvestment = new Big(0);
let timeWeightedGrossPerformancePercentage = new Big(1); let timeWeightedGrossPerformancePercentage = new Big(1);
let timeWeightedNetPerformancePercentage = new Big(1); let timeWeightedNetPerformancePercentage = new Big(1);
let totalInvestment = new Big(0); let totalInvestment = new Big(0);
let totalInvestmentWithGrossPerformanceFromSell = new Big(0);
let totalUnits = new Big(0); let totalUnits = new Big(0);
const holdingPeriodPerformances: {
grossReturn: Big;
netReturn: Big;
valueOfInvestment: Big;
}[] = [];
// Add a synthetic order at the start and the end date // Add a synthetic order at the start and the end date
orders.push({ orders.push({
symbol, symbol,
@ -688,7 +696,7 @@ export class PortfolioCalculatorNew {
name: '', name: '',
quantity: new Big(0), quantity: new Big(0),
type: TypeOfOrder.BUY, type: TypeOfOrder.BUY,
unitPrice: unitPriceAtStartDate ?? new Big(0) unitPrice: unitPriceAtStartDate
}); });
orders.push({ orders.push({
@ -701,7 +709,7 @@ export class PortfolioCalculatorNew {
name: '', name: '',
quantity: new Big(0), quantity: new Big(0),
type: TypeOfOrder.BUY, type: TypeOfOrder.BUY,
unitPrice: unitPriceAtEndDate ?? new Big(0) unitPrice: unitPriceAtEndDate
}); });
// Sort orders so that the start and end placeholder order are at the right // Sort orders so that the start and end placeholder order are at the right
@ -724,9 +732,31 @@ export class PortfolioCalculatorNew {
return order.itemType === 'start'; return order.itemType === 'start';
}); });
const indexOfEndOrder = orders.findIndex((order) => {
return order.itemType === 'end';
});
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];
if (order.itemType === 'start') {
// Take the unit price of the order as the market price if there are no
// orders of this symbol before the start date
order.unitPrice =
indexOfStartOrder === 0
? orders[i + 1]?.unitPrice
: unitPriceAtStartDate;
}
// Calculate the average start price as soon as any units are held
if (
averagePriceAtStartDate.eq(0) &&
i >= indexOfStartOrder &&
totalUnits.gt(0)
) {
averagePriceAtStartDate = totalInvestment.div(totalUnits);
}
const valueOfInvestmentBeforeTransaction = totalUnits.mul( const valueOfInvestmentBeforeTransaction = totalUnits.mul(
order.unitPrice order.unitPrice
); );
@ -735,12 +765,25 @@ export class PortfolioCalculatorNew {
.mul(order.unitPrice) .mul(order.unitPrice)
.mul(this.getFactor(order.type)); .mul(this.getFactor(order.type));
if ( totalInvestment = totalInvestment.plus(transactionInvestment);
!initialValue &&
order.itemType !== 'start' && if (totalInvestment.gt(maxTotalInvestment)) {
order.itemType !== 'end' maxTotalInvestment = totalInvestment;
) { }
initialValue = transactionInvestment;
if (i === indexOfEndOrder && totalUnits.gt(0)) {
averagePriceAtEndDate = totalInvestment.div(totalUnits);
}
if (i >= indexOfStartOrder && !initialValue) {
if (
i === indexOfStartOrder &&
!valueOfInvestmentBeforeTransaction.eq(0)
) {
initialValue = valueOfInvestmentBeforeTransaction;
} else if (transactionInvestment.gt(0)) {
initialValue = transactionInvestment;
}
} }
fees = fees.plus(order.fee); fees = fees.plus(order.fee);
@ -760,16 +803,17 @@ export class PortfolioCalculatorNew {
grossPerformanceFromSell grossPerformanceFromSell
); );
totalInvestment = totalInvestment totalInvestmentWithGrossPerformanceFromSell =
.plus(transactionInvestment) totalInvestmentWithGrossPerformanceFromSell
.plus(grossPerformanceFromSell); .plus(transactionInvestment)
.plus(grossPerformanceFromSell);
lastAveragePrice = totalUnits.eq(0) lastAveragePrice = totalUnits.eq(0)
? new Big(0) ? new Big(0)
: totalInvestment.div(totalUnits); : totalInvestmentWithGrossPerformanceFromSell.div(totalUnits);
const newGrossPerformance = valueOfInvestment const newGrossPerformance = valueOfInvestment
.minus(totalInvestment) .minus(totalInvestmentWithGrossPerformanceFromSell)
.plus(grossPerformanceFromSells); .plus(grossPerformanceFromSells);
if ( if (
@ -812,14 +856,6 @@ export class PortfolioCalculatorNew {
timeWeightedNetPerformancePercentage.mul( timeWeightedNetPerformancePercentage.mul(
new Big(1).plus(netHoldingPeriodReturn) new Big(1).plus(netHoldingPeriodReturn)
); );
holdingPeriodPerformances.push({
grossReturn: grossHoldingPeriodReturn,
netReturn: netHoldingPeriodReturn,
valueOfInvestment: lastValueOfInvestmentBeforeTransaction.plus(
lastTransactionInvestment
)
});
} }
grossPerformance = newGrossPerformance; grossPerformance = newGrossPerformance;
@ -849,39 +885,63 @@ export class PortfolioCalculatorNew {
.minus(grossPerformanceAtStartDate) .minus(grossPerformanceAtStartDate)
.minus(fees.minus(feesAtStartDate)); .minus(fees.minus(feesAtStartDate));
let valueOfInvestmentSum = new Big(0); const grossPerformancePercentage =
averagePriceAtStartDate.eq(0) ||
for (const holdingPeriodPerformance of holdingPeriodPerformances) { averagePriceAtEndDate.eq(0) ||
valueOfInvestmentSum = valueOfInvestmentSum.plus( orders[indexOfStartOrder].unitPrice.eq(0)
holdingPeriodPerformance.valueOfInvestment ? totalGrossPerformance.div(maxTotalInvestment)
); : unitPriceAtEndDate
} .div(averagePriceAtEndDate)
.div(
let totalWeightedGrossPerformance = new Big(0); orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
let totalWeightedNetPerformance = new Big(0); )
.minus(1);
// Weight the holding period returns according to their value of investment
for (const holdingPeriodPerformance of holdingPeriodPerformances) { const feesPerUnit = totalUnits.gt(0)
totalWeightedGrossPerformance = totalWeightedGrossPerformance.plus( ? fees.minus(feesAtStartDate).div(totalUnits)
holdingPeriodPerformance.grossReturn : new Big(0);
.mul(holdingPeriodPerformance.valueOfInvestment)
.div(valueOfInvestmentSum) const netPerformancePercentage =
); averagePriceAtStartDate.eq(0) ||
averagePriceAtEndDate.eq(0) ||
totalWeightedNetPerformance = totalWeightedNetPerformance.plus( orders[indexOfStartOrder].unitPrice.eq(0)
holdingPeriodPerformance.netReturn ? totalNetPerformance.div(maxTotalInvestment)
.mul(holdingPeriodPerformance.valueOfInvestment) : unitPriceAtEndDate
.div(valueOfInvestmentSum) .minus(feesPerUnit)
.div(averagePriceAtEndDate)
.div(
orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
)
.minus(1);
if (PortfolioCalculatorNew.ENABLE_LOGGING) {
console.log(
`
${symbol}
Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed(
2
)} -> ${unitPriceAtEndDate.toFixed(2)}
Average price: ${averagePriceAtStartDate.toFixed(
2
)} -> ${averagePriceAtEndDate.toFixed(2)}
Max. total investment: ${maxTotalInvestment.toFixed(2)}
Gross performance: ${totalGrossPerformance.toFixed(
2
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}%
Fees per unit: ${feesPerUnit.toFixed(2)}
Net performance: ${totalNetPerformance.toFixed(
2
)} / ${netPerformancePercentage.mul(100).toFixed(2)}%`
); );
} }
return { return {
initialValue, initialValue,
hasErrors: !initialValue || !unitPriceAtEndDate, grossPerformancePercentage,
netPerformancePercentage,
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
netPerformance: totalNetPerformance, netPerformance: totalNetPerformance,
netPerformancePercentage: totalWeightedNetPerformance, grossPerformance: totalGrossPerformance
grossPerformance: totalGrossPerformance,
grossPerformancePercentage: totalWeightedGrossPerformance
}; };
} }

Loading…
Cancel
Save