|
|
|
@ -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[];
|
|
|
|
@ -228,7 +230,7 @@ export class PortfolioCalculatorNew {
|
|
|
|
|
const initialValues: { [symbol: string]: Big } = {};
|
|
|
|
|
|
|
|
|
|
const positions: TimelinePosition[] = [];
|
|
|
|
|
let hasErrorsInSymbolMetrics = false;
|
|
|
|
|
let hasAnySymbolMetricsErrors = false;
|
|
|
|
|
|
|
|
|
|
for (const item of lastTransactionPoint.items) {
|
|
|
|
|
const marketValue = marketSymbolMap[todayString]?.[item.symbol];
|
|
|
|
@ -246,8 +248,7 @@ export class PortfolioCalculatorNew {
|
|
|
|
|
symbol: item.symbol
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
hasErrorsInSymbolMetrics = hasErrorsInSymbolMetrics || hasErrors;
|
|
|
|
|
|
|
|
|
|
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
|
|
|
|
|
initialValues[item.symbol] = initialValue;
|
|
|
|
|
|
|
|
|
|
positions.push({
|
|
|
|
@ -272,12 +273,13 @@ export class PortfolioCalculatorNew {
|
|
|
|
|
transactionCount: item.transactionCount
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const overall = this.calculateOverallPerformance(positions, initialValues);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...overall,
|
|
|
|
|
positions,
|
|
|
|
|
hasErrors: hasErrorsInSymbolMetrics || overall.hasErrors
|
|
|
|
|
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -395,16 +397,16 @@ export class PortfolioCalculatorNew {
|
|
|
|
|
|
|
|
|
|
private calculateOverallPerformance(
|
|
|
|
|
positions: TimelinePosition[],
|
|
|
|
|
initialValues: { [p: string]: Big }
|
|
|
|
|
initialValues: { [symbol: string]: Big }
|
|
|
|
|
) {
|
|
|
|
|
let hasErrors = false;
|
|
|
|
|
let currentValue = new Big(0);
|
|
|
|
|
let totalInvestment = new Big(0);
|
|
|
|
|
let grossPerformance = new Big(0);
|
|
|
|
|
let grossPerformancePercentage = new Big(0);
|
|
|
|
|
let hasErrors = false;
|
|
|
|
|
let netPerformance = 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) {
|
|
|
|
|
if (currentPosition.marketPrice) {
|
|
|
|
@ -414,27 +416,34 @@ export class PortfolioCalculatorNew {
|
|
|
|
|
} else {
|
|
|
|
|
hasErrors = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
totalInvestment = totalInvestment.plus(currentPosition.investment);
|
|
|
|
|
|
|
|
|
|
if (currentPosition.grossPerformance) {
|
|
|
|
|
grossPerformance = grossPerformance.plus(
|
|
|
|
|
currentPosition.grossPerformance
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
netPerformance = netPerformance.plus(currentPosition.netPerformance);
|
|
|
|
|
} else if (!currentPosition.quantity.eq(0)) {
|
|
|
|
|
hasErrors = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
currentPosition.grossPerformancePercentage &&
|
|
|
|
|
initialValues[currentPosition.symbol]
|
|
|
|
|
) {
|
|
|
|
|
const currentInitialValue = initialValues[currentPosition.symbol];
|
|
|
|
|
completeInitialValue = completeInitialValue.plus(currentInitialValue);
|
|
|
|
|
if (currentPosition.grossPerformancePercentage) {
|
|
|
|
|
// Use the average from the initial value and the current investment as
|
|
|
|
|
// a weight
|
|
|
|
|
const weight = (initialValues[currentPosition.symbol] ?? new Big(0))
|
|
|
|
|
.plus(currentPosition.investment)
|
|
|
|
|
.div(2);
|
|
|
|
|
|
|
|
|
|
sumOfWeights = sumOfWeights.plus(weight);
|
|
|
|
|
|
|
|
|
|
grossPerformancePercentage = grossPerformancePercentage.plus(
|
|
|
|
|
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
|
|
|
|
|
currentPosition.grossPerformancePercentage.mul(weight)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
netPerformancePercentage = netPerformancePercentage.plus(
|
|
|
|
|
currentPosition.netPerformancePercentage.mul(currentInitialValue)
|
|
|
|
|
currentPosition.netPerformancePercentage.mul(weight)
|
|
|
|
|
);
|
|
|
|
|
} else if (!currentPosition.quantity.eq(0)) {
|
|
|
|
|
Logger.warn(
|
|
|
|
@ -444,11 +453,12 @@ export class PortfolioCalculatorNew {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!completeInitialValue.eq(0)) {
|
|
|
|
|
grossPerformancePercentage =
|
|
|
|
|
grossPerformancePercentage.div(completeInitialValue);
|
|
|
|
|
netPerformancePercentage =
|
|
|
|
|
netPerformancePercentage.div(completeInitialValue);
|
|
|
|
|
if (sumOfWeights.gt(0)) {
|
|
|
|
|
grossPerformancePercentage = grossPerformancePercentage.div(sumOfWeights);
|
|
|
|
|
netPerformancePercentage = netPerformancePercentage.div(sumOfWeights);
|
|
|
|
|
} else {
|
|
|
|
|
grossPerformancePercentage = new Big(0);
|
|
|
|
|
netPerformancePercentage = new Big(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
@ -657,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);
|
|
|
|
@ -666,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,
|
|
|
|
@ -688,7 +696,7 @@ export class PortfolioCalculatorNew {
|
|
|
|
|
name: '',
|
|
|
|
|
quantity: new Big(0),
|
|
|
|
|
type: TypeOfOrder.BUY,
|
|
|
|
|
unitPrice: unitPriceAtStartDate ?? new Big(0)
|
|
|
|
|
unitPrice: unitPriceAtStartDate
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
orders.push({
|
|
|
|
@ -701,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
|
|
|
|
@ -724,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
|
|
|
|
|
);
|
|
|
|
@ -735,12 +765,25 @@ export class PortfolioCalculatorNew {
|
|
|
|
|
.mul(order.unitPrice)
|
|
|
|
|
.mul(this.getFactor(order.type));
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!initialValue &&
|
|
|
|
|
order.itemType !== 'start' &&
|
|
|
|
|
order.itemType !== 'end'
|
|
|
|
|
) {
|
|
|
|
|
initialValue = transactionInvestment;
|
|
|
|
|
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 &&
|
|
|
|
|
!valueOfInvestmentBeforeTransaction.eq(0)
|
|
|
|
|
) {
|
|
|
|
|
initialValue = valueOfInvestmentBeforeTransaction;
|
|
|
|
|
} else if (transactionInvestment.gt(0)) {
|
|
|
|
|
initialValue = transactionInvestment;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fees = fees.plus(order.fee);
|
|
|
|
@ -760,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 (
|
|
|
|
@ -812,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;
|
|
|
|
@ -849,39 +885,63 @@ 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.eq(0) ||
|
|
|
|
|
orders[indexOfStartOrder].unitPrice.eq(0)
|
|
|
|
|
? totalNetPerformance.div(maxTotalInvestment)
|
|
|
|
|
: unitPriceAtEndDate
|
|
|
|
|
.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 {
|
|
|
|
|
initialValue,
|
|
|
|
|
hasErrors: !initialValue || !unitPriceAtEndDate,
|
|
|
|
|
grossPerformancePercentage,
|
|
|
|
|
netPerformancePercentage,
|
|
|
|
|
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
|
|
|
|
|
netPerformance: totalNetPerformance,
|
|
|
|
|
netPerformancePercentage: totalWeightedNetPerformance,
|
|
|
|
|
grossPerformance: totalGrossPerformance,
|
|
|
|
|
grossPerformancePercentage: totalWeightedGrossPerformance
|
|
|
|
|
grossPerformance: totalGrossPerformance
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|