Browse Source

Task/refactor average investment of ROAI calculator (#8078)

Refactor average investment calculation in ROAI calculator
pull/8083/head
Thomas Kaul 2 days ago
committed by GitHub
parent
commit
817c6775de
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  2. 222
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  3. 6
      apps/api/src/app/portfolio/interfaces/average-investment-item.interface.ts
  4. 8
      apps/api/src/app/portfolio/interfaces/weighted-investment.interface.ts

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

@ -264,7 +264,7 @@ describe('PortfolioCalculator', () => {
'2017': {
date: '2017-12-31',
netPerformance: 26957.033439,
netPerformanceInPercentage: 44.10965416175681,
netPerformanceInPercentage: 44.10965416175668,
netPerformanceInPercentageWithCurrencyEffect: 43.148434302822835,
netPerformanceWithCurrencyEffect: 27081.23736,
totalInvestmentValueWithCurrencyEffect: 318.54266729999995,
@ -273,8 +273,8 @@ describe('PortfolioCalculator', () => {
max: {
date: '2018-01-01',
netPerformance: 26458.9121202,
netPerformanceInPercentage: 42.43911719562077,
netPerformanceInPercentageWithCurrencyEffect: 41.65910103572173,
netPerformanceInPercentage: 42.439117195620575,
netPerformanceInPercentageWithCurrencyEffect: 41.65910103572163,
netPerformanceWithCurrencyEffect: 26516.2087014,
totalInvestmentValueWithCurrencyEffect: 318.54266729999995,
valueWithCurrencyEffect: 13298.425356
@ -346,7 +346,7 @@ describe('PortfolioCalculator', () => {
// Other weights give another rounding of the floating point numbers
expect(
performanceByDateRange['2017'].netPerformanceInPercentage
).toBeCloseTo(44.10965416175681, 10);
).toBeCloseTo(44.10965416175668, 10);
expect(
performanceByDateRange['2017']
@ -354,13 +354,13 @@ describe('PortfolioCalculator', () => {
).toBeCloseTo(43.148434302822835, 10);
expect(performanceByDateRange.max.netPerformanceInPercentage).toBeCloseTo(
42.43911719562077,
42.439117195620575,
10
);
expect(
performanceByDateRange.max.netPerformanceInPercentageWithCurrencyEffect
).toBeCloseTo(41.65910103572173, 10);
).toBeCloseTo(41.65910103572163, 10);
});
});
});

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

@ -1,8 +1,10 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface';
import { AverageInvestmentItem } from '@ghostfolio/api/app/portfolio/interfaces/average-investment-item.interface';
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface';
import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity-item.interface';
import { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface';
import { WeightedInvestment } from '@ghostfolio/api/app/portfolio/interfaces/weighted-investment.interface';
import { PerformancePercentages } from '@ghostfolio/api/app/portfolio/types/performance-percentages.type';
import {
getAnnualizedPerformancePercent,
@ -29,6 +31,7 @@ import {
isBefore,
isThisYear
} from 'date-fns';
import { sortedIndex } from 'lodash-es';
export class RoaiPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance(
@ -209,77 +212,60 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}: {
historicalDataItems: HistoricalDataItem[];
}): { [date: string]: PerformancePercentages } {
let grossPerformanceAtStartDate: number;
let grossPerformanceWithCurrencyEffectAtStartDate: number;
let previousDate: Date;
let sumOfWeightedInvestments = 0;
let sumOfWeightedInvestmentsWithCurrencyEffect = 0;
let totalInvestmentDays = 0;
let totalInvestmentDaysWithCurrencyEffect = 0;
const performancePercentagesByDate: {
[date: string]: PerformancePercentages;
} = {};
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
// range
if (index === 0) {
grossPerformanceAtStartDate =
historicalDataItem.value - historicalDataItem.totalInvestment;
grossPerformanceWithCurrencyEffectAtStartDate =
historicalDataItem.valueWithCurrencyEffect -
historicalDataItem.totalInvestmentValueWithCurrencyEffect;
} else {
// The chart dates are not evenly spaced, and the investment changes on
// a chart date only. Thus the investment of the previous chart date
// applies to each day until this chart date.
const previousHistoricalDataItem = historicalDataItems[index - 1];
if (historicalDataItems.length === 0) {
return performancePercentagesByDate;
}
const daysSincePreviousChartDate = differenceInDays(date, previousDate);
const parsedDates = historicalDataItems.map(({ date }) => {
return parseDate(date);
});
if (previousHistoricalDataItem.totalInvestment > 0) {
sumOfWeightedInvestments +=
(previousHistoricalDataItem.totalInvestment +
grossPerformanceAtStartDate) *
daysSincePreviousChartDate;
const daysUntilNextItem = parsedDates.slice(1).map((nextDate, index) => {
return differenceInDays(nextDate, parsedDates[index]);
});
totalInvestmentDays += daysSincePreviousChartDate;
}
// Take the values at the start date from the first day of the date range
const [firstHistoricalDataItem] = historicalDataItems;
if (
previousHistoricalDataItem.totalInvestmentValueWithCurrencyEffect > 0
) {
sumOfWeightedInvestmentsWithCurrencyEffect +=
(previousHistoricalDataItem.totalInvestmentValueWithCurrencyEffect +
grossPerformanceWithCurrencyEffectAtStartDate) *
daysSincePreviousChartDate;
const weightedInvestments = this.getWeightedInvestments({
grossPerformanceAtStartDate: new Big(firstHistoricalDataItem.value).minus(
firstHistoricalDataItem.totalInvestment
),
items: historicalDataItems.map(({ totalInvestment }, index) => {
return {
daysUntilNextItem: daysUntilNextItem[index],
investment: new Big(totalInvestment)
};
})
});
totalInvestmentDaysWithCurrencyEffect += daysSincePreviousChartDate;
}
const weightedInvestmentsWithCurrencyEffect = this.getWeightedInvestments({
grossPerformanceAtStartDate: new Big(
firstHistoricalDataItem.valueWithCurrencyEffect
).minus(firstHistoricalDataItem.totalInvestmentValueWithCurrencyEffect),
items: historicalDataItems.map(
({ totalInvestmentValueWithCurrencyEffect }, index) => {
return {
daysUntilNextItem: daysUntilNextItem[index],
investment: new Big(totalInvestmentValueWithCurrencyEffect)
};
}
// 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
// first day, and subsequent buy and sell activities stay included.
const averageInvestmentValue = this.getAverageInvestment({
grossPerformanceAtStartDate,
sumOfWeightedInvestments,
totalInvestmentDays,
investment: historicalDataItem.totalInvestment
)
});
for (const [index, historicalDataItem] of historicalDataItems.entries()) {
const averageInvestmentValue = this.getAverageInvestment(
weightedInvestments[index]
).toNumber();
const averageInvestmentValueWithCurrencyEffect =
this.getAverageInvestment({
grossPerformanceAtStartDate:
grossPerformanceWithCurrencyEffectAtStartDate,
investment: historicalDataItem.totalInvestmentValueWithCurrencyEffect,
sumOfWeightedInvestments: sumOfWeightedInvestmentsWithCurrencyEffect,
totalInvestmentDays: totalInvestmentDaysWithCurrencyEffect
});
this.getAverageInvestment(
weightedInvestmentsWithCurrencyEffect[index]
).toNumber();
performancePercentagesByDate[historicalDataItem.date] = {
dividendInPercentageWithCurrencyEffect:
@ -297,8 +283,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
averageInvestmentValueWithCurrencyEffect
: 0
};
previousDate = date;
}
return performancePercentagesByDate;
@ -657,46 +641,35 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
investmentValuesAccumulatedAtStartDateWithCurrencyEffect
);
let average = new Big(0);
let dayCount = 0;
const averageInvestmentItems: AverageInvestmentItem[] = [];
for (let i = chartDates.length - 1; i >= 0; i -= 1) {
for (
let i = sortedIndex(chartDates, rangeStartDateString);
i < chartDates.length;
i += 1
) {
const date = chartDates[i];
if (date > rangeEndDateString) {
continue;
} else if (date < rangeStartDateString) {
break;
}
const isLastChartDateOfRange =
i === chartDates.length - 1 || chartDates[i + 1] > rangeEndDateString;
// 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
// each day until the next chart date. The investment of the last chart
// date of the range counts for this day only.
const investmentDays = isLastChartDateOfRange
? 1
: daysUntilNextChartDate[i];
if (
investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big &&
investmentValuesAccumulatedWithCurrencyEffect[date].gt(0)
) {
average = average.add(
investmentValuesAccumulatedWithCurrencyEffect[date]
.add(grossPerformanceAtDateRangeStartWithCurrencyEffect)
.mul(investmentDays)
);
dayCount += investmentDays;
}
averageInvestmentItems.push({
daysUntilNextItem: daysUntilNextChartDate[i],
investment:
investmentValuesAccumulatedWithCurrencyEffect[date] ?? new Big(0)
});
}
if (dayCount > 0) {
average = average.div(dayCount);
}
const weightedInvestment = this.getWeightedInvestments({
grossPerformanceAtStartDate:
grossPerformanceAtDateRangeStartWithCurrencyEffect,
items: averageInvestmentItems
}).at(-1);
const average = weightedInvestment
? this.getAverageInvestment(weightedInvestment)
: new Big(0);
netPerformanceWithCurrencyEffectMap[dateRange] =
netPerformanceValuesWithCurrencyEffect[rangeEndDateString]?.minus(
@ -797,23 +770,64 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
investment,
sumOfWeightedInvestments,
totalInvestmentDays
}: WeightedInvestment) {
// The investment of the current item counts for this day only
if (investment.gt(0)) {
return sumOfWeightedInvestments
.add(investment.add(grossPerformanceAtStartDate))
.div(totalInvestmentDays + 1);
}
return totalInvestmentDays > 0
? sumOfWeightedInvestments.div(totalInvestmentDays)
: new Big(0);
}
/**
* Returns the sum of the weighted investments and the number of investment
* days before each item of a date range. The chart dates are not evenly
* spaced, and the investment changes on a chart date only. Thus the
* investment of an item applies to each day until the next item.
*
* The gross performance at the start date of the range is added to each
* investment. Thus the range starts with the value of its first day, and
* subsequent buy and sell activities stay included.
*/
private getWeightedInvestments({
grossPerformanceAtStartDate,
items
}: {
grossPerformanceAtStartDate: number;
investment: number;
sumOfWeightedInvestments: number;
totalInvestmentDays: number;
grossPerformanceAtStartDate: Big;
items: AverageInvestmentItem[];
}) {
// The investment of the current chart date counts for this day only
if (investment > 0) {
return (
(sumOfWeightedInvestments +
(investment + grossPerformanceAtStartDate)) /
(totalInvestmentDays + 1)
const weightedInvestments: WeightedInvestment[] = [];
let sumOfWeightedInvestments = new Big(0);
let totalInvestmentDays = 0;
for (const [index, { investment }] of items.entries()) {
if (index > 0) {
const previousItem = items[index - 1];
if (previousItem.investment.gt(0)) {
sumOfWeightedInvestments = sumOfWeightedInvestments.add(
previousItem.investment
.add(grossPerformanceAtStartDate)
.mul(previousItem.daysUntilNextItem)
);
totalInvestmentDays += previousItem.daysUntilNextItem;
}
}
return totalInvestmentDays > 0
? sumOfWeightedInvestments / totalInvestmentDays
: 0;
weightedInvestments.push({
grossPerformanceAtStartDate,
investment,
sumOfWeightedInvestments,
totalInvestmentDays
});
}
return weightedInvestments;
}
}

6
apps/api/src/app/portfolio/interfaces/average-investment-item.interface.ts

@ -0,0 +1,6 @@
import { Big } from 'big.js';
export interface AverageInvestmentItem {
daysUntilNextItem: number;
investment: Big;
}

8
apps/api/src/app/portfolio/interfaces/weighted-investment.interface.ts

@ -0,0 +1,8 @@
import { Big } from 'big.js';
export interface WeightedInvestment {
grossPerformanceAtStartDate: Big;
investment: Big;
sumOfWeightedInvestments: Big;
totalInvestmentDays: number;
}
Loading…
Cancel
Save