Browse Source

Fix time weighted average investment in ROAI calculator

pull/8074/head
Thomas Kaul 1 week ago
parent
commit
564c6511bd
  1. 1
      apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
  2. 14
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 27
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  4. 87
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  5. 1
      apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts
  6. 1
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

1
apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts

@ -24,6 +24,7 @@ export class MwrPortfolioCalculator extends PortfolioCalculator {
protected getHoldingPerformance({}: { protected getHoldingPerformance({}: {
chartDates: string[]; chartDates: string[];
daysUntilNextChartDate: { [date: string]: number };
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {

14
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -362,6 +362,17 @@ export abstract class PortfolioCalculator {
return chartDate; return chartDate;
}); });
// The chart dates are the same for each holding and each date range. Thus
// calculate the number of days until the next chart date one time only.
const daysUntilNextChartDate: { [date: string]: number } = {};
for (let i = 0; i < chartDates.length - 1; i += 1) {
daysUntilNextChartDate[chartDates[i]] = differenceInDays(
parseDate(chartDates[i + 1]),
parseDate(chartDates[i])
);
}
const errors: ResponseError['errors'] = []; const errors: ResponseError['errors'] = [];
let hasAnyHoldingPerformanceErrors = false; let hasAnyHoldingPerformanceErrors = false;
@ -438,6 +449,7 @@ export abstract class PortfolioCalculator {
totalLiabilitiesInBaseCurrency totalLiabilitiesInBaseCurrency
} = this.getHoldingPerformance({ } = this.getHoldingPerformance({
chartDates, chartDates,
daysUntilNextChartDate,
marketSymbolMap, marketSymbolMap,
dataSource: item.dataSource, dataSource: item.dataSource,
end: this.endDate, end: this.endDate,
@ -935,6 +947,7 @@ export abstract class PortfolioCalculator {
protected abstract getHoldingPerformance({ protected abstract getHoldingPerformance({
chartDates, chartDates,
dataSource, dataSource,
daysUntilNextChartDate,
end, end,
exchangeRates, exchangeRates,
marketSymbolMap, marketSymbolMap,
@ -942,6 +955,7 @@ export abstract class PortfolioCalculator {
symbol symbol
}: { }: {
chartDates: string[]; chartDates: string[];
daysUntilNextChartDate: { [date: string]: number };
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {

27
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts

@ -112,7 +112,7 @@ describe('PortfolioCalculator', () => {
const performanceByDateRange = await getPerformanceByDateRange({ const performanceByDateRange = await getPerformanceByDateRange({
portfolioCalculator, portfolioCalculator,
dateRanges: ['1d', 'max', 'ytd'] dateRanges: ['1d', '2017', 'max', 'ytd']
}); });
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
@ -162,6 +162,7 @@ describe('PortfolioCalculator', () => {
netPerformancePercentage: new Big('42.41978276196153750666'), netPerformancePercentage: new Big('42.41978276196153750666'),
netPerformancePercentageWithCurrencyEffectMap: { netPerformancePercentageWithCurrencyEffectMap: {
'1d': new Big('-0.04016229506406263535'), '1d': new Big('-0.04016229506406263535'),
'2017': new Big('43.14843430282283692638'),
max: new Big('41.65910103572163194783'), max: new Big('41.65910103572163194783'),
ytd: new Big('-0.04016229506406263535') ytd: new Big('-0.04016229506406263535')
}, },
@ -258,6 +259,15 @@ describe('PortfolioCalculator', () => {
totalInvestmentValueWithCurrencyEffect: 318.54266729999995, totalInvestmentValueWithCurrencyEffect: 318.54266729999995,
valueWithCurrencyEffect: 13298.425356 valueWithCurrencyEffect: 13298.425356
}, },
'2017': {
date: '2017-12-31',
netPerformance: 26957.033439,
netPerformanceInPercentage: 44.109822737327406,
netPerformanceInPercentageWithCurrencyEffect: 43.14859823223876,
netPerformanceWithCurrencyEffect: 27081.23736,
totalInvestmentValueWithCurrencyEffect: 318.54266729999995,
valueWithCurrencyEffect: 13854.86868
},
max: { max: {
date: '2018-01-01', date: '2018-01-01',
netPerformance: 26458.9121202, netPerformance: 26458.9121202,
@ -285,7 +295,8 @@ describe('PortfolioCalculator', () => {
const environmentConfigurationService = new ConfigurationService(); const environmentConfigurationService = new ConfigurationService();
// Fewer chart items give fewer chart dates, which must not change the // Fewer chart items give fewer chart dates, which must not change the
// average investment // average investment. The range 2017 starts with a gross performance
// and ends with the sell.
jest.spyOn(configurationService, 'get').mockImplementation((key) => { jest.spyOn(configurationService, 'get').mockImplementation((key) => {
return key === 'MAX_CHART_ITEMS' return key === 'MAX_CHART_ITEMS'
? 50 ? 50
@ -308,17 +319,27 @@ describe('PortfolioCalculator', () => {
const performanceByDateRange = await getPerformanceByDateRange({ const performanceByDateRange = await getPerformanceByDateRange({
portfolioCalculator, portfolioCalculator,
dateRanges: ['max'] dateRanges: ['2017', 'max']
}); });
expect( expect(
portfolioSnapshot.positions[0] portfolioSnapshot.positions[0]
.netPerformancePercentageWithCurrencyEffectMap .netPerformancePercentageWithCurrencyEffectMap
).toMatchObject({ ).toMatchObject({
'2017': new Big('43.14843430282283692638'),
max: new Big('41.65910103572163194783') max: new Big('41.65910103572163194783')
}); });
// Other weights give another rounding of the floating point numbers // Other weights give another rounding of the floating point numbers
expect(
performanceByDateRange['2017'].netPerformanceInPercentage
).toBeCloseTo(44.109822737327406, 10);
expect(
performanceByDateRange['2017']
.netPerformanceInPercentageWithCurrencyEffect
).toBeCloseTo(43.14859823223876, 10);
expect(performanceByDateRange.max.netPerformanceInPercentage).toBeCloseTo( expect(performanceByDateRange.max.netPerformanceInPercentage).toBeCloseTo(
42.43911719562077, 42.43911719562077,
10 10

87
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

@ -211,6 +211,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}): { [date: string]: PerformancePercentages } { }): { [date: string]: PerformancePercentages } {
let grossPerformanceAtStartDate: number; let grossPerformanceAtStartDate: number;
let grossPerformanceWithCurrencyEffectAtStartDate: number; let grossPerformanceWithCurrencyEffectAtStartDate: number;
let previousDate: Date;
let sumOfWeightedInvestments = 0; let sumOfWeightedInvestments = 0;
let sumOfWeightedInvestmentsWithCurrencyEffect = 0; let sumOfWeightedInvestmentsWithCurrencyEffect = 0;
let totalInvestmentDays = 0; let totalInvestmentDays = 0;
@ -221,6 +222,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
} = {}; } = {};
for (const [index, historicalDataItem] of historicalDataItems.entries()) { for (const [index, historicalDataItem] of historicalDataItems.entries()) {
const date = parseDate(historicalDataItem.date);
// Take the values at the start date from the first day of the date // Take the values at the start date from the first day of the date
// range // range
if (index === 0) { if (index === 0) {
@ -236,10 +239,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
// applies to each day until this chart date. // applies to each day until this chart date.
const previousHistoricalDataItem = historicalDataItems[index - 1]; const previousHistoricalDataItem = historicalDataItems[index - 1];
const daysSincePreviousChartDate = differenceInDays( const daysSincePreviousChartDate = differenceInDays(date, previousDate);
parseDate(historicalDataItem.date),
parseDate(previousHistoricalDataItem.date)
);
if (previousHistoricalDataItem.totalInvestment > 0) { if (previousHistoricalDataItem.totalInvestment > 0) {
sumOfWeightedInvestments += sumOfWeightedInvestments +=
@ -264,28 +264,22 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
// Add the gross performance at the start date of the range to the // Add the gross performance at the start date of the range to the
// investment of each day. Thus the range starts with the value of its // investment of each day. Thus the range starts with the value of its
// first day, and subsequent buy and sell activities stay included. The // first day, and subsequent buy and sell activities stay included.
// investment of this chart date counts for this day only. const averageInvestmentValue = this.getAverageInvestment({
const averageInvestmentValue = grossPerformanceAtStartDate,
historicalDataItem.totalInvestment > 0 sumOfWeightedInvestments,
? (sumOfWeightedInvestments + totalInvestmentDays,
(historicalDataItem.totalInvestment + investment: historicalDataItem.totalInvestment
grossPerformanceAtStartDate)) / });
(totalInvestmentDays + 1)
: totalInvestmentDays > 0
? sumOfWeightedInvestments / totalInvestmentDays
: 0;
const averageInvestmentValueWithCurrencyEffect = const averageInvestmentValueWithCurrencyEffect =
historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0 this.getAverageInvestment({
? (sumOfWeightedInvestmentsWithCurrencyEffect + grossPerformanceAtStartDate:
(historicalDataItem.totalInvestmentValueWithCurrencyEffect + grossPerformanceWithCurrencyEffectAtStartDate,
grossPerformanceWithCurrencyEffectAtStartDate)) / investment: historicalDataItem.totalInvestmentValueWithCurrencyEffect,
(totalInvestmentDaysWithCurrencyEffect + 1) sumOfWeightedInvestments: sumOfWeightedInvestmentsWithCurrencyEffect,
: totalInvestmentDaysWithCurrencyEffect > 0 totalInvestmentDays: totalInvestmentDaysWithCurrencyEffect
? sumOfWeightedInvestmentsWithCurrencyEffect / });
totalInvestmentDaysWithCurrencyEffect
: 0;
performancePercentagesByDate[historicalDataItem.date] = { performancePercentagesByDate[historicalDataItem.date] = {
dividendInPercentageWithCurrencyEffect: dividendInPercentageWithCurrencyEffect:
@ -303,6 +297,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
averageInvestmentValueWithCurrencyEffect averageInvestmentValueWithCurrencyEffect
: 0 : 0
}; };
previousDate = date;
} }
return performancePercentagesByDate; return performancePercentagesByDate;
@ -311,6 +307,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
protected getHoldingPerformance({ protected getHoldingPerformance({
chartDates, chartDates,
dataSource, dataSource,
daysUntilNextChartDate,
end, end,
exchangeRates, exchangeRates,
marketSymbolMap, marketSymbolMap,
@ -318,6 +315,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
symbol symbol
}: { }: {
chartDates: string[]; chartDates: string[];
daysUntilNextChartDate: { [date: string]: number };
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {
@ -661,7 +659,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let average = new Big(0); let average = new Big(0);
let dayCount = 0; let dayCount = 0;
let nextChartDateString: string;
for (let i = chartDates.length - 1; i >= 0; i -= 1) { for (let i = chartDates.length - 1; i >= 0; i -= 1) {
const date = chartDates[i]; const date = chartDates[i];
@ -672,15 +669,16 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
break; break;
} }
const isLastChartDateOfRange =
i === chartDates.length - 1 || chartDates[i + 1] > rangeEndDateString;
// The chart dates are not evenly spaced, and the investment changes // The chart dates are not evenly spaced, and the investment changes
// on a chart date only. Thus the investment of a chart date applies to // on a chart date only. Thus the investment of a chart date applies to
// each day until the next chart date. The investment of the last chart // each day until the next chart date. The investment of the last chart
// date of the range counts for this day only. // date of the range counts for this day only.
const daysUntilNextChartDate = nextChartDateString const investmentDays = isLastChartDateOfRange
? differenceInDays(parseDate(nextChartDateString), parseDate(date)) ? 1
: 1; : daysUntilNextChartDate[date];
nextChartDateString = date;
if ( if (
investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big &&
@ -689,10 +687,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
average = average.add( average = average.add(
investmentValuesAccumulatedWithCurrencyEffect[date] investmentValuesAccumulatedWithCurrencyEffect[date]
.add(grossPerformanceAtDateRangeStartWithCurrencyEffect) .add(grossPerformanceAtDateRangeStartWithCurrencyEffect)
.mul(daysUntilNextChartDate) .mul(investmentDays)
); );
dayCount += daysUntilNextChartDate; dayCount += investmentDays;
} }
} }
@ -793,4 +791,29 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
protected getPerformanceCalculationType() { protected getPerformanceCalculationType() {
return PerformanceCalculationType.ROAI; return PerformanceCalculationType.ROAI;
} }
private getAverageInvestment({
grossPerformanceAtStartDate,
investment,
sumOfWeightedInvestments,
totalInvestmentDays
}: {
grossPerformanceAtStartDate: number;
investment: number;
sumOfWeightedInvestments: number;
totalInvestmentDays: number;
}) {
// The investment of the current chart date counts for this day only
if (investment > 0) {
return (
(sumOfWeightedInvestments +
(investment + grossPerformanceAtStartDate)) /
(totalInvestmentDays + 1)
);
}
return totalInvestmentDays > 0
? sumOfWeightedInvestments / totalInvestmentDays
: 0;
}
} }

1
apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts

@ -24,6 +24,7 @@ export class RoiPortfolioCalculator extends PortfolioCalculator {
protected getHoldingPerformance({}: { protected getHoldingPerformance({}: {
chartDates: string[]; chartDates: string[];
daysUntilNextChartDate: { [date: string]: number };
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {

1
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

@ -24,6 +24,7 @@ export class TwrPortfolioCalculator extends PortfolioCalculator {
protected getHoldingPerformance({}: { protected getHoldingPerformance({}: {
chartDates: string[]; chartDates: string[];
daysUntilNextChartDate: { [date: string]: number };
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {

Loading…
Cancel
Save