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': { '2017': {
date: '2017-12-31', date: '2017-12-31',
netPerformance: 26957.033439, netPerformance: 26957.033439,
netPerformanceInPercentage: 44.10965416175681, netPerformanceInPercentage: 44.10965416175668,
netPerformanceInPercentageWithCurrencyEffect: 43.148434302822835, netPerformanceInPercentageWithCurrencyEffect: 43.148434302822835,
netPerformanceWithCurrencyEffect: 27081.23736, netPerformanceWithCurrencyEffect: 27081.23736,
totalInvestmentValueWithCurrencyEffect: 318.54266729999995, totalInvestmentValueWithCurrencyEffect: 318.54266729999995,
@ -273,8 +273,8 @@ describe('PortfolioCalculator', () => {
max: { max: {
date: '2018-01-01', date: '2018-01-01',
netPerformance: 26458.9121202, netPerformance: 26458.9121202,
netPerformanceInPercentage: 42.43911719562077, netPerformanceInPercentage: 42.439117195620575,
netPerformanceInPercentageWithCurrencyEffect: 41.65910103572173, netPerformanceInPercentageWithCurrencyEffect: 41.65910103572163,
netPerformanceWithCurrencyEffect: 26516.2087014, netPerformanceWithCurrencyEffect: 26516.2087014,
totalInvestmentValueWithCurrencyEffect: 318.54266729999995, totalInvestmentValueWithCurrencyEffect: 318.54266729999995,
valueWithCurrencyEffect: 13298.425356 valueWithCurrencyEffect: 13298.425356
@ -346,7 +346,7 @@ describe('PortfolioCalculator', () => {
// Other weights give another rounding of the floating point numbers // Other weights give another rounding of the floating point numbers
expect( expect(
performanceByDateRange['2017'].netPerformanceInPercentage performanceByDateRange['2017'].netPerformanceInPercentage
).toBeCloseTo(44.10965416175681, 10); ).toBeCloseTo(44.10965416175668, 10);
expect( expect(
performanceByDateRange['2017'] performanceByDateRange['2017']
@ -354,13 +354,13 @@ describe('PortfolioCalculator', () => {
).toBeCloseTo(43.148434302822835, 10); ).toBeCloseTo(43.148434302822835, 10);
expect(performanceByDateRange.max.netPerformanceInPercentage).toBeCloseTo( expect(performanceByDateRange.max.netPerformanceInPercentage).toBeCloseTo(
42.43911719562077, 42.439117195620575,
10 10
); );
expect( expect(
performanceByDateRange.max.netPerformanceInPercentageWithCurrencyEffect 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 { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface'; 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 { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface';
import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity-item.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 { 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 { PerformancePercentages } from '@ghostfolio/api/app/portfolio/types/performance-percentages.type';
import { import {
getAnnualizedPerformancePercent, getAnnualizedPerformancePercent,
@ -29,6 +31,7 @@ import {
isBefore, isBefore,
isThisYear isThisYear
} from 'date-fns'; } from 'date-fns';
import { sortedIndex } from 'lodash-es';
export class RoaiPortfolioCalculator extends PortfolioCalculator { export class RoaiPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance( protected calculateOverallPerformance(
@ -209,77 +212,60 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}: { }: {
historicalDataItems: HistoricalDataItem[]; historicalDataItems: HistoricalDataItem[];
}): { [date: string]: PerformancePercentages } { }): { [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: { const performancePercentagesByDate: {
[date: string]: PerformancePercentages; [date: string]: PerformancePercentages;
} = {}; } = {};
for (const [index, historicalDataItem] of historicalDataItems.entries()) { if (historicalDataItems.length === 0) {
const date = parseDate(historicalDataItem.date); return performancePercentagesByDate;
}
// 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];
const daysSincePreviousChartDate = differenceInDays(date, previousDate); const parsedDates = historicalDataItems.map(({ date }) => {
return parseDate(date);
});
if (previousHistoricalDataItem.totalInvestment > 0) { const daysUntilNextItem = parsedDates.slice(1).map((nextDate, index) => {
sumOfWeightedInvestments += return differenceInDays(nextDate, parsedDates[index]);
(previousHistoricalDataItem.totalInvestment + });
grossPerformanceAtStartDate) *
daysSincePreviousChartDate;
totalInvestmentDays += daysSincePreviousChartDate; // Take the values at the start date from the first day of the date range
} const [firstHistoricalDataItem] = historicalDataItems;
if ( const weightedInvestments = this.getWeightedInvestments({
previousHistoricalDataItem.totalInvestmentValueWithCurrencyEffect > 0 grossPerformanceAtStartDate: new Big(firstHistoricalDataItem.value).minus(
) { firstHistoricalDataItem.totalInvestment
sumOfWeightedInvestmentsWithCurrencyEffect += ),
(previousHistoricalDataItem.totalInvestmentValueWithCurrencyEffect + items: historicalDataItems.map(({ totalInvestment }, index) => {
grossPerformanceWithCurrencyEffectAtStartDate) * return {
daysSincePreviousChartDate; 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 = const averageInvestmentValueWithCurrencyEffect =
this.getAverageInvestment({ this.getAverageInvestment(
grossPerformanceAtStartDate: weightedInvestmentsWithCurrencyEffect[index]
grossPerformanceWithCurrencyEffectAtStartDate, ).toNumber();
investment: historicalDataItem.totalInvestmentValueWithCurrencyEffect,
sumOfWeightedInvestments: sumOfWeightedInvestmentsWithCurrencyEffect,
totalInvestmentDays: totalInvestmentDaysWithCurrencyEffect
});
performancePercentagesByDate[historicalDataItem.date] = { performancePercentagesByDate[historicalDataItem.date] = {
dividendInPercentageWithCurrencyEffect: dividendInPercentageWithCurrencyEffect:
@ -297,8 +283,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
averageInvestmentValueWithCurrencyEffect averageInvestmentValueWithCurrencyEffect
: 0 : 0
}; };
previousDate = date;
} }
return performancePercentagesByDate; return performancePercentagesByDate;
@ -657,46 +641,35 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
investmentValuesAccumulatedAtStartDateWithCurrencyEffect investmentValuesAccumulatedAtStartDateWithCurrencyEffect
); );
let average = new Big(0); const averageInvestmentItems: AverageInvestmentItem[] = [];
let dayCount = 0;
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]; const date = chartDates[i];
if (date > rangeEndDateString) { if (date > rangeEndDateString) {
continue;
} else if (date < rangeStartDateString) {
break; break;
} }
const isLastChartDateOfRange = averageInvestmentItems.push({
i === chartDates.length - 1 || chartDates[i + 1] > rangeEndDateString; daysUntilNextItem: daysUntilNextChartDate[i],
investment:
// The chart dates are not evenly spaced, and the investment changes investmentValuesAccumulatedWithCurrencyEffect[date] ?? new Big(0)
// 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;
}
} }
if (dayCount > 0) { const weightedInvestment = this.getWeightedInvestments({
average = average.div(dayCount); grossPerformanceAtStartDate:
} grossPerformanceAtDateRangeStartWithCurrencyEffect,
items: averageInvestmentItems
}).at(-1);
const average = weightedInvestment
? this.getAverageInvestment(weightedInvestment)
: new Big(0);
netPerformanceWithCurrencyEffectMap[dateRange] = netPerformanceWithCurrencyEffectMap[dateRange] =
netPerformanceValuesWithCurrencyEffect[rangeEndDateString]?.minus( netPerformanceValuesWithCurrencyEffect[rangeEndDateString]?.minus(
@ -797,23 +770,64 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
investment, investment,
sumOfWeightedInvestments, sumOfWeightedInvestments,
totalInvestmentDays 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; grossPerformanceAtStartDate: Big;
investment: number; items: AverageInvestmentItem[];
sumOfWeightedInvestments: number;
totalInvestmentDays: number;
}) { }) {
// The investment of the current chart date counts for this day only const weightedInvestments: WeightedInvestment[] = [];
if (investment > 0) {
return ( let sumOfWeightedInvestments = new Big(0);
(sumOfWeightedInvestments + let totalInvestmentDays = 0;
(investment + grossPerformanceAtStartDate)) /
(totalInvestmentDays + 1) 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 weightedInvestments.push({
? sumOfWeightedInvestments / totalInvestmentDays grossPerformanceAtStartDate,
: 0; 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