Improve calculation of overall performance percentage (#701)
* Improve calculation of overall performance percentage Co-authored-by: Reto Kaul <retokaul@sublimd.com>
This commit is contained in:
parent
2a2a5f4da5
commit
a5771f601d
@ -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) ||
|
||||||
|
averagePriceAtEndDate.eq(0) ||
|
||||||
|
orders[indexOfStartOrder].unitPrice.eq(0)
|
||||||
|
? totalGrossPerformance.div(maxTotalInvestment)
|
||||||
|
: unitPriceAtEndDate
|
||||||
|
.div(averagePriceAtEndDate)
|
||||||
|
.div(
|
||||||
|
orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
|
||||||
|
)
|
||||||
|
.minus(1);
|
||||||
|
|
||||||
for (const holdingPeriodPerformance of holdingPeriodPerformances) {
|
const feesPerUnit = totalUnits.gt(0)
|
||||||
valueOfInvestmentSum = valueOfInvestmentSum.plus(
|
? fees.minus(feesAtStartDate).div(totalUnits)
|
||||||
holdingPeriodPerformance.valueOfInvestment
|
: new Big(0);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalWeightedGrossPerformance = new Big(0);
|
const netPerformancePercentage =
|
||||||
let totalWeightedNetPerformance = new Big(0);
|
averagePriceAtStartDate.eq(0) ||
|
||||||
|
averagePriceAtEndDate.eq(0) ||
|
||||||
|
orders[indexOfStartOrder].unitPrice.eq(0)
|
||||||
|
? totalNetPerformance.div(maxTotalInvestment)
|
||||||
|
: unitPriceAtEndDate
|
||||||
|
.minus(feesPerUnit)
|
||||||
|
.div(averagePriceAtEndDate)
|
||||||
|
.div(
|
||||||
|
orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
|
||||||
|
)
|
||||||
|
.minus(1);
|
||||||
|
|
||||||
// Weight the holding period returns according to their value of investment
|
if (PortfolioCalculatorNew.ENABLE_LOGGING) {
|
||||||
for (const holdingPeriodPerformance of holdingPeriodPerformances) {
|
console.log(
|
||||||
totalWeightedGrossPerformance = totalWeightedGrossPerformance.plus(
|
`
|
||||||
holdingPeriodPerformance.grossReturn
|
${symbol}
|
||||||
.mul(holdingPeriodPerformance.valueOfInvestment)
|
Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed(
|
||||||
.div(valueOfInvestmentSum)
|
2
|
||||||
);
|
)} -> ${unitPriceAtEndDate.toFixed(2)}
|
||||||
|
Average price: ${averagePriceAtStartDate.toFixed(
|
||||||
totalWeightedNetPerformance = totalWeightedNetPerformance.plus(
|
2
|
||||||
holdingPeriodPerformance.netReturn
|
)} -> ${averagePriceAtEndDate.toFixed(2)}
|
||||||
.mul(holdingPeriodPerformance.valueOfInvestment)
|
Max. total investment: ${maxTotalInvestment.toFixed(2)}
|
||||||
.div(valueOfInvestmentSum)
|
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…
x
Reference in New Issue
Block a user