diff --git a/apps/api/src/app/portfolio/portfolio-calculator-new.ts b/apps/api/src/app/portfolio/portfolio-calculator-new.ts index 4dcf1bddf..d5a861dc3 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-new.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-new.ts @@ -33,6 +33,8 @@ import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.in import { TransactionPoint } from './interfaces/transaction-point.interface'; export class PortfolioCalculatorNew { + private static readonly ENABLE_LOGGING = false; + private currency: string; private currentRateService: CurrentRateService; private orders: PortfolioOrder[]; @@ -665,6 +667,8 @@ export class PortfolioCalculatorNew { }; } + let averagePriceAtEndDate = new Big(0); + let averagePriceAtStartDate = new Big(0); let feesAtStartDate = new Big(0); let fees = new Big(0); let grossPerformance = new Big(0); @@ -674,17 +678,13 @@ export class PortfolioCalculatorNew { let lastAveragePrice = new Big(0); let lastTransactionInvestment = new Big(0); let lastValueOfInvestmentBeforeTransaction = new Big(0); + let maxTotalInvestment = new Big(0); let timeWeightedGrossPerformancePercentage = new Big(1); let timeWeightedNetPerformancePercentage = new Big(1); let totalInvestment = new Big(0); + let totalInvestmentWithGrossPerformanceFromSell = 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 orders.push({ symbol, @@ -696,7 +696,7 @@ export class PortfolioCalculatorNew { name: '', quantity: new Big(0), type: TypeOfOrder.BUY, - unitPrice: unitPriceAtStartDate ?? new Big(0) + unitPrice: unitPriceAtStartDate }); orders.push({ @@ -709,7 +709,7 @@ export class PortfolioCalculatorNew { name: '', quantity: new Big(0), type: TypeOfOrder.BUY, - unitPrice: unitPriceAtEndDate ?? new Big(0) + unitPrice: unitPriceAtEndDate }); // Sort orders so that the start and end placeholder order are at the right @@ -732,9 +732,31 @@ export class PortfolioCalculatorNew { return order.itemType === 'start'; }); + const indexOfEndOrder = orders.findIndex((order) => { + return order.itemType === 'end'; + }); + for (let i = 0; i < orders.length; i += 1) { 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( order.unitPrice ); @@ -743,6 +765,16 @@ export class PortfolioCalculatorNew { .mul(order.unitPrice) .mul(this.getFactor(order.type)); + totalInvestment = totalInvestment.plus(transactionInvestment); + + if (totalInvestment.gt(maxTotalInvestment)) { + maxTotalInvestment = totalInvestment; + } + + if (i === indexOfEndOrder && totalUnits.gt(0)) { + averagePriceAtEndDate = totalInvestment.div(totalUnits); + } + if (i >= indexOfStartOrder && !initialValue) { if ( i === indexOfStartOrder && @@ -771,16 +803,17 @@ export class PortfolioCalculatorNew { grossPerformanceFromSell ); - totalInvestment = totalInvestment - .plus(transactionInvestment) - .plus(grossPerformanceFromSell); + totalInvestmentWithGrossPerformanceFromSell = + totalInvestmentWithGrossPerformanceFromSell + .plus(transactionInvestment) + .plus(grossPerformanceFromSell); lastAveragePrice = totalUnits.eq(0) ? new Big(0) - : totalInvestment.div(totalUnits); + : totalInvestmentWithGrossPerformanceFromSell.div(totalUnits); const newGrossPerformance = valueOfInvestment - .minus(totalInvestment) + .minus(totalInvestmentWithGrossPerformanceFromSell) .plus(grossPerformanceFromSells); if ( @@ -823,14 +856,6 @@ export class PortfolioCalculatorNew { timeWeightedNetPerformancePercentage.mul( new Big(1).plus(netHoldingPeriodReturn) ); - - holdingPeriodPerformances.push({ - grossReturn: grossHoldingPeriodReturn, - netReturn: netHoldingPeriodReturn, - valueOfInvestment: lastValueOfInvestmentBeforeTransaction.plus( - lastTransactionInvestment - ) - }); } grossPerformance = newGrossPerformance; @@ -860,39 +885,61 @@ export class PortfolioCalculatorNew { .minus(grossPerformanceAtStartDate) .minus(fees.minus(feesAtStartDate)); - let valueOfInvestmentSum = new Big(0); - - for (const holdingPeriodPerformance of holdingPeriodPerformances) { - valueOfInvestmentSum = valueOfInvestmentSum.plus( - holdingPeriodPerformance.valueOfInvestment - ); - } - - let totalWeightedGrossPerformance = new Big(0); - let totalWeightedNetPerformance = new Big(0); - - // Weight the holding period returns according to their value of investment - for (const holdingPeriodPerformance of holdingPeriodPerformances) { - totalWeightedGrossPerformance = totalWeightedGrossPerformance.plus( - holdingPeriodPerformance.grossReturn - .mul(holdingPeriodPerformance.valueOfInvestment) - .div(valueOfInvestmentSum) - ); - - totalWeightedNetPerformance = totalWeightedNetPerformance.plus( - holdingPeriodPerformance.netReturn - .mul(holdingPeriodPerformance.valueOfInvestment) - .div(valueOfInvestmentSum) + 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); + + const feesPerUnit = totalUnits.gt(0) + ? fees.minus(feesAtStartDate).div(totalUnits) + : new Big(0); + + const netPerformancePercentage = + averagePriceAtStartDate.eq(0) || + averagePriceAtEndDate.plus(feesPerUnit).eq(0) || + orders[indexOfStartOrder].unitPrice.eq(0) + ? totalNetPerformance.div(maxTotalInvestment) + : unitPriceAtEndDate + .div(averagePriceAtEndDate.plus(feesPerUnit)) + .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)}% + Net performance: ${totalNetPerformance.toFixed( + 2 + )} / ${netPerformancePercentage.mul(100).toFixed(2)}%` ); } return { initialValue, + grossPerformancePercentage, + netPerformancePercentage, hasErrors: !initialValue || !unitPriceAtEndDate, netPerformance: totalNetPerformance, - netPerformancePercentage: totalWeightedNetPerformance, - grossPerformance: totalGrossPerformance, - grossPerformancePercentage: totalWeightedGrossPerformance + grossPerformance: totalGrossPerformance }; }