Time weighted portfolio performance calculation (#2778)
* Implement time weighted portfolio performance calculation * Update changelog
This commit is contained in:
parent
7d68905f1b
commit
b183c45027
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Changed the performance calculation to a time-weighted approach
|
||||||
- Used the `HasPermission` annotation in endpoints
|
- Used the `HasPermission` annotation in endpoints
|
||||||
|
|
||||||
## 2.32.0 - 2023-12-26
|
## 2.32.0 - 2023-12-26
|
||||||
|
@ -92,6 +92,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
marketPrice: 148.9,
|
marketPrice: 148.9,
|
||||||
quantity: new Big('0'),
|
quantity: new Big('0'),
|
||||||
symbol: 'BALN.SW',
|
symbol: 'BALN.SW',
|
||||||
|
timeWeightedInvestment: new Big('285.8'),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -81,6 +81,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
marketPrice: 148.9,
|
marketPrice: 148.9,
|
||||||
quantity: new Big('2'),
|
quantity: new Big('2'),
|
||||||
symbol: 'BALN.SW',
|
symbol: 'BALN.SW',
|
||||||
|
timeWeightedInvestment: new Big('273.2'),
|
||||||
transactionCount: 1
|
transactionCount: 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -73,10 +73,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentValue: new Big('13657.2'),
|
currentValue: new Big('13657.2'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('27172.74'),
|
grossPerformance: new Big('27172.74'),
|
||||||
grossPerformancePercentage: new Big('42.40043067128546016291'),
|
grossPerformancePercentage: new Big('42.41978276196153750666'),
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
netPerformance: new Big('27172.74'),
|
netPerformance: new Big('27172.74'),
|
||||||
netPerformancePercentage: new Big('42.40043067128546016291'),
|
netPerformancePercentage: new Big('42.41978276196153750666'),
|
||||||
positions: [
|
positions: [
|
||||||
{
|
{
|
||||||
averagePrice: new Big('320.43'),
|
averagePrice: new Big('320.43'),
|
||||||
@ -85,13 +85,14 @@ describe('PortfolioCalculator', () => {
|
|||||||
fee: new Big('0'),
|
fee: new Big('0'),
|
||||||
firstBuyDate: '2015-01-01',
|
firstBuyDate: '2015-01-01',
|
||||||
grossPerformance: new Big('27172.74'),
|
grossPerformance: new Big('27172.74'),
|
||||||
grossPerformancePercentage: new Big('42.40043067128546016291'),
|
grossPerformancePercentage: new Big('42.41978276196153750666'),
|
||||||
investment: new Big('320.43'),
|
investment: new Big('320.43'),
|
||||||
netPerformance: new Big('27172.74'),
|
netPerformance: new Big('27172.74'),
|
||||||
netPerformancePercentage: new Big('42.40043067128546016291'),
|
netPerformancePercentage: new Big('42.41978276196153750666'),
|
||||||
marketPrice: 13657.2,
|
marketPrice: 13657.2,
|
||||||
quantity: new Big('1'),
|
quantity: new Big('1'),
|
||||||
symbol: 'BTCUSD',
|
symbol: 'BTCUSD',
|
||||||
|
timeWeightedInvestment: new Big('640.56763686131386861314'),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -73,10 +73,10 @@ describe('PortfolioCalculator', () => {
|
|||||||
currentValue: new Big('87.8'),
|
currentValue: new Big('87.8'),
|
||||||
errors: [],
|
errors: [],
|
||||||
grossPerformance: new Big('21.93'),
|
grossPerformance: new Big('21.93'),
|
||||||
grossPerformancePercentage: new Big('0.14465699208443271768'),
|
grossPerformancePercentage: new Big('0.15113417083448194384'),
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
netPerformance: new Big('17.68'),
|
netPerformance: new Big('17.68'),
|
||||||
netPerformancePercentage: new Big('0.11662269129287598945'),
|
netPerformancePercentage: new Big('0.12184460284330327256'),
|
||||||
positions: [
|
positions: [
|
||||||
{
|
{
|
||||||
averagePrice: new Big('75.80'),
|
averagePrice: new Big('75.80'),
|
||||||
@ -85,13 +85,14 @@ describe('PortfolioCalculator', () => {
|
|||||||
fee: new Big('4.25'),
|
fee: new Big('4.25'),
|
||||||
firstBuyDate: '2022-03-07',
|
firstBuyDate: '2022-03-07',
|
||||||
grossPerformance: new Big('21.93'),
|
grossPerformance: new Big('21.93'),
|
||||||
grossPerformancePercentage: new Big('0.14465699208443271768'),
|
grossPerformancePercentage: new Big('0.15113417083448194384'),
|
||||||
investment: new Big('75.80'),
|
investment: new Big('75.80'),
|
||||||
netPerformance: new Big('17.68'),
|
netPerformance: new Big('17.68'),
|
||||||
netPerformancePercentage: new Big('0.11662269129287598945'),
|
netPerformancePercentage: new Big('0.12184460284330327256'),
|
||||||
marketPrice: 87.8,
|
marketPrice: 87.8,
|
||||||
quantity: new Big('1'),
|
quantity: new Big('1'),
|
||||||
symbol: 'NOVN.SW',
|
symbol: 'NOVN.SW',
|
||||||
|
timeWeightedInvestment: new Big('145.10285714285714285714'),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -112,6 +112,7 @@ describe('PortfolioCalculator', () => {
|
|||||||
marketPrice: 87.8,
|
marketPrice: 87.8,
|
||||||
quantity: new Big('0'),
|
quantity: new Big('0'),
|
||||||
symbol: 'NOVN.SW',
|
symbol: 'NOVN.SW',
|
||||||
|
timeWeightedInvestment: new Big('151.6'),
|
||||||
transactionCount: 2
|
transactionCount: 2
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
addMilliseconds,
|
addMilliseconds,
|
||||||
addMonths,
|
addMonths,
|
||||||
addYears,
|
addYears,
|
||||||
|
differenceInDays,
|
||||||
endOfDay,
|
endOfDay,
|
||||||
format,
|
format,
|
||||||
isAfter,
|
isAfter,
|
||||||
@ -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,
|
||||||
@ -447,7 +464,6 @@ export class PortfolioCalculator {
|
|||||||
if (firstIndex > 0) {
|
if (firstIndex > 0) {
|
||||||
firstIndex--;
|
firstIndex--;
|
||||||
}
|
}
|
||||||
const initialValues: { [symbol: string]: Big } = {};
|
|
||||||
|
|
||||||
const positions: TimelinePosition[] = [];
|
const positions: TimelinePosition[] = [];
|
||||||
let hasAnySymbolMetricsErrors = false;
|
let hasAnySymbolMetricsErrors = false;
|
||||||
@ -461,9 +477,9 @@ export class PortfolioCalculator {
|
|||||||
grossPerformance,
|
grossPerformance,
|
||||||
grossPerformancePercentage,
|
grossPerformancePercentage,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
initialValue,
|
|
||||||
netPerformance,
|
netPerformance,
|
||||||
netPerformancePercentage
|
netPerformancePercentage,
|
||||||
|
timeWeightedInvestment
|
||||||
} = this.getSymbolMetrics({
|
} = this.getSymbolMetrics({
|
||||||
end,
|
end,
|
||||||
marketSymbolMap,
|
marketSymbolMap,
|
||||||
@ -472,9 +488,9 @@ export class PortfolioCalculator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
|
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
|
||||||
initialValues[item.symbol] = initialValue;
|
|
||||||
|
|
||||||
positions.push({
|
positions.push({
|
||||||
|
timeWeightedInvestment,
|
||||||
averagePrice: item.quantity.eq(0)
|
averagePrice: item.quantity.eq(0)
|
||||||
? new Big(0)
|
? new Big(0)
|
||||||
: item.investment.div(item.quantity),
|
: item.investment.div(item.quantity),
|
||||||
@ -509,7 +525,7 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const overall = this.calculateOverallPerformance(positions, initialValues);
|
const overall = this.calculateOverallPerformance(positions);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...overall,
|
...overall,
|
||||||
@ -732,18 +748,13 @@ export class PortfolioCalculator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateOverallPerformance(
|
private calculateOverallPerformance(positions: TimelinePosition[]) {
|
||||||
positions: TimelinePosition[],
|
|
||||||
initialValues: { [symbol: string]: Big }
|
|
||||||
) {
|
|
||||||
let currentValue = new Big(0);
|
let currentValue = new Big(0);
|
||||||
let grossPerformance = new Big(0);
|
let grossPerformance = new Big(0);
|
||||||
let grossPerformancePercentage = new Big(0);
|
|
||||||
let hasErrors = false;
|
let hasErrors = false;
|
||||||
let netPerformance = new Big(0);
|
let netPerformance = new Big(0);
|
||||||
let netPerformancePercentage = new Big(0);
|
|
||||||
let sumOfWeights = new Big(0);
|
|
||||||
let totalInvestment = new Big(0);
|
let totalInvestment = new Big(0);
|
||||||
|
let totalTimeWeightedInvestment = new Big(0);
|
||||||
|
|
||||||
for (const currentPosition of positions) {
|
for (const currentPosition of positions) {
|
||||||
if (currentPosition.marketPrice) {
|
if (currentPosition.marketPrice) {
|
||||||
@ -766,21 +777,9 @@ export class PortfolioCalculator {
|
|||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPosition.grossPerformancePercentage) {
|
if (currentPosition.timeWeightedInvestment) {
|
||||||
// Use the average from the initial value and the current investment as
|
totalTimeWeightedInvestment = totalTimeWeightedInvestment.plus(
|
||||||
// a weight
|
currentPosition.timeWeightedInvestment
|
||||||
const weight = (initialValues[currentPosition.symbol] ?? new Big(0))
|
|
||||||
.plus(currentPosition.investment)
|
|
||||||
.div(2);
|
|
||||||
|
|
||||||
sumOfWeights = sumOfWeights.plus(weight);
|
|
||||||
|
|
||||||
grossPerformancePercentage = grossPerformancePercentage.plus(
|
|
||||||
currentPosition.grossPerformancePercentage.mul(weight)
|
|
||||||
);
|
|
||||||
|
|
||||||
netPerformancePercentage = netPerformancePercentage.plus(
|
|
||||||
currentPosition.netPerformancePercentage.mul(weight)
|
|
||||||
);
|
);
|
||||||
} else if (!currentPosition.quantity.eq(0)) {
|
} else if (!currentPosition.quantity.eq(0)) {
|
||||||
Logger.warn(
|
Logger.warn(
|
||||||
@ -791,22 +790,18 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sumOfWeights.gt(0)) {
|
|
||||||
grossPerformancePercentage = grossPerformancePercentage.div(sumOfWeights);
|
|
||||||
netPerformancePercentage = netPerformancePercentage.div(sumOfWeights);
|
|
||||||
} else {
|
|
||||||
grossPerformancePercentage = new Big(0);
|
|
||||||
netPerformancePercentage = new Big(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentValue,
|
currentValue,
|
||||||
grossPerformance,
|
grossPerformance,
|
||||||
grossPerformancePercentage,
|
|
||||||
hasErrors,
|
hasErrors,
|
||||||
netPerformance,
|
netPerformance,
|
||||||
netPerformancePercentage,
|
totalInvestment,
|
||||||
totalInvestment
|
netPerformancePercentage: totalTimeWeightedInvestment.eq(0)
|
||||||
|
? new Big(0)
|
||||||
|
: netPerformance.div(totalTimeWeightedInvestment),
|
||||||
|
grossPerformancePercentage: totalTimeWeightedInvestment.eq(0)
|
||||||
|
? new Big(0)
|
||||||
|
: grossPerformance.div(totalTimeWeightedInvestment)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1018,6 +1013,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 +1021,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 +1118,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];
|
||||||
|
|
||||||
@ -1162,11 +1161,11 @@ export class PortfolioCalculator {
|
|||||||
order.type === 'BUY'
|
order.type === 'BUY'
|
||||||
? order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
|
? order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
|
||||||
: totalUnits.gt(0)
|
: totalUnits.gt(0)
|
||||||
? totalInvestment
|
? totalInvestment
|
||||||
.div(totalUnits)
|
.div(totalUnits)
|
||||||
.mul(order.quantity)
|
.mul(order.quantity)
|
||||||
.mul(this.getFactor(order.type))
|
.mul(this.getFactor(order.type))
|
||||||
: new Big(0);
|
: new Big(0);
|
||||||
|
|
||||||
if (PortfolioCalculator.ENABLE_LOGGING) {
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
console.log('totalInvestment', totalInvestment.toNumber());
|
console.log('totalInvestment', totalInvestment.toNumber());
|
||||||
@ -1174,6 +1173,7 @@ export class PortfolioCalculator {
|
|||||||
console.log('transactionInvestment', transactionInvestment.toNumber());
|
console.log('transactionInvestment', transactionInvestment.toNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalInvestmentBeforeTransaction = totalInvestment;
|
||||||
totalInvestment = totalInvestment.plus(transactionInvestment);
|
totalInvestment = totalInvestment.plus(transactionInvestment);
|
||||||
|
|
||||||
if (i >= indexOfStartOrder && totalInvestment.gt(maxTotalInvestment)) {
|
if (i >= indexOfStartOrder && totalInvestment.gt(maxTotalInvestment)) {
|
||||||
@ -1243,14 +1243,48 @@ export class PortfolioCalculator {
|
|||||||
grossPerformanceAtStartDate = grossPerformance;
|
grossPerformanceAtStartDate = grossPerformance;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isChartMode && i > indexOfStartOrder) {
|
if (i > indexOfStartOrder) {
|
||||||
currentValues[order.date] = valueOfInvestment;
|
// Only consider periods with an investment for the calculation of
|
||||||
netPerformanceValues[order.date] = grossPerformance
|
// the time weighted investment
|
||||||
.minus(grossPerformanceAtStartDate)
|
if (totalInvestmentBeforeTransaction.gt(0)) {
|
||||||
.minus(fees.minus(feesAtStartDate));
|
// Calculate the number of days since the previous order
|
||||||
|
const orderDate = new Date(order.date);
|
||||||
|
const previousOrderDate = new Date(orders[i - 1].date);
|
||||||
|
|
||||||
investmentValues[order.date] = totalInvestment;
|
let daysSinceLastOrder = differenceInDays(
|
||||||
maxInvestmentValues[order.date] = maxTotalInvestment;
|
orderDate,
|
||||||
|
previousOrderDate
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set to at least 1 day, otherwise the transactions on the same day
|
||||||
|
// would not be considered in the time weighted calculation
|
||||||
|
if (daysSinceLastOrder <= 0) {
|
||||||
|
daysSinceLastOrder = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum up the total investment days since the start date to calculate
|
||||||
|
// the time weighted investment
|
||||||
|
totalInvestmentDays += daysSinceLastOrder;
|
||||||
|
|
||||||
|
sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add(
|
||||||
|
totalInvestmentBeforeTransaction.mul(daysSinceLastOrder)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChartMode) {
|
||||||
|
currentValues[order.date] = valueOfInvestment;
|
||||||
|
netPerformanceValues[order.date] = grossPerformance
|
||||||
|
.minus(grossPerformanceAtStartDate)
|
||||||
|
.minus(fees.minus(feesAtStartDate));
|
||||||
|
|
||||||
|
investmentValues[order.date] = totalInvestment;
|
||||||
|
maxInvestmentValues[order.date] = maxTotalInvestment;
|
||||||
|
|
||||||
|
timeWeightedInvestmentValues[order.date] =
|
||||||
|
totalInvestmentDays > 0
|
||||||
|
? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
|
||||||
|
: new Big(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PortfolioCalculator.ENABLE_LOGGING) {
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
@ -1274,50 +1308,79 @@ 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 ||
|
|
||||||
averagePriceAtStartDate.eq(0) ||
|
if (
|
||||||
averagePriceAtEndDate.eq(0) ||
|
PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_TIME_WEIGHTED_INVESTMENT
|
||||||
orders[indexOfStartOrder].unitPrice.eq(0)
|
) {
|
||||||
? maxInvestmentBetweenStartAndEndDate.gt(0)
|
grossPerformancePercentage =
|
||||||
? totalGrossPerformance.div(maxInvestmentBetweenStartAndEndDate)
|
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0)
|
||||||
: new Big(0)
|
? totalGrossPerformance.div(
|
||||||
: // This formula has the issue that buying more units with a price
|
timeWeightedAverageInvestmentBetweenStartAndEndDate
|
||||||
// lower than the average buying price results in a positive
|
|
||||||
// performance even if the market price stays constant
|
|
||||||
unitPriceAtEndDate
|
|
||||||
.div(averagePriceAtEndDate)
|
|
||||||
.div(
|
|
||||||
orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
|
|
||||||
)
|
)
|
||||||
.minus(1);
|
: new Big(0);
|
||||||
|
} else {
|
||||||
|
grossPerformancePercentage =
|
||||||
|
averagePriceAtStartDate.eq(0) ||
|
||||||
|
averagePriceAtEndDate.eq(0) ||
|
||||||
|
orders[indexOfStartOrder].unitPrice.eq(0)
|
||||||
|
? maxInvestmentBetweenStartAndEndDate.gt(0)
|
||||||
|
? totalGrossPerformance.div(maxInvestmentBetweenStartAndEndDate)
|
||||||
|
: new Big(0)
|
||||||
|
: // This formula has the issue that buying more units with a price
|
||||||
|
// lower than the average buying price results in a positive
|
||||||
|
// performance even if the market price stays constant
|
||||||
|
unitPriceAtEndDate
|
||||||
|
.div(averagePriceAtEndDate)
|
||||||
|
.div(
|
||||||
|
orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
|
||||||
|
)
|
||||||
|
.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 ||
|
|
||||||
averagePriceAtStartDate.eq(0) ||
|
if (
|
||||||
averagePriceAtEndDate.eq(0) ||
|
PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_TIME_WEIGHTED_INVESTMENT
|
||||||
orders[indexOfStartOrder].unitPrice.eq(0)
|
) {
|
||||||
? maxInvestmentBetweenStartAndEndDate.gt(0)
|
netPerformancePercentage =
|
||||||
? totalNetPerformance.div(maxInvestmentBetweenStartAndEndDate)
|
timeWeightedAverageInvestmentBetweenStartAndEndDate.gt(0)
|
||||||
: new Big(0)
|
? totalNetPerformance.div(
|
||||||
: // This formula has the issue that buying more units with a price
|
timeWeightedAverageInvestmentBetweenStartAndEndDate
|
||||||
// lower than the average buying price results in a positive
|
|
||||||
// performance even if the market price stays constant
|
|
||||||
unitPriceAtEndDate
|
|
||||||
.minus(feesPerUnit)
|
|
||||||
.div(averagePriceAtEndDate)
|
|
||||||
.div(
|
|
||||||
orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
|
|
||||||
)
|
)
|
||||||
.minus(1);
|
: new Big(0);
|
||||||
|
} else {
|
||||||
|
netPerformancePercentage =
|
||||||
|
averagePriceAtStartDate.eq(0) ||
|
||||||
|
averagePriceAtEndDate.eq(0) ||
|
||||||
|
orders[indexOfStartOrder].unitPrice.eq(0)
|
||||||
|
? maxInvestmentBetweenStartAndEndDate.gt(0)
|
||||||
|
? totalNetPerformance.div(maxInvestmentBetweenStartAndEndDate)
|
||||||
|
: new Big(0)
|
||||||
|
: // This formula has the issue that buying more units with a price
|
||||||
|
// lower than the average buying price results in a positive
|
||||||
|
// performance even if the market price stays constant
|
||||||
|
unitPriceAtEndDate
|
||||||
|
.minus(feesPerUnit)
|
||||||
|
.div(averagePriceAtEndDate)
|
||||||
|
.div(
|
||||||
|
orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)
|
||||||
|
)
|
||||||
|
.minus(1);
|
||||||
|
}
|
||||||
|
|
||||||
if (PortfolioCalculator.ENABLE_LOGGING) {
|
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||||
console.log(
|
console.log(
|
||||||
@ -1330,6 +1393,9 @@ 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(
|
||||||
|
2
|
||||||
|
)}
|
||||||
Max. total investment: ${maxTotalInvestment.toFixed(2)}
|
Max. total investment: ${maxTotalInvestment.toFixed(2)}
|
||||||
Gross performance: ${totalGrossPerformance.toFixed(
|
Gross performance: ${totalGrossPerformance.toFixed(
|
||||||
2
|
2
|
||||||
@ -1349,9 +1415,12 @@ 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,
|
||||||
|
timeWeightedInvestment:
|
||||||
|
timeWeightedAverageInvestmentBetweenStartAndEndDate
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,5 +16,6 @@ export interface TimelinePosition {
|
|||||||
quantity: Big;
|
quantity: Big;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
tags?: Tag[];
|
tags?: Tag[];
|
||||||
|
timeWeightedInvestment: Big;
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user