Browse Source

Fix time weighted average investment in ROAI calculator

pull/8074/head
Thomas Kaul 1 week ago
parent
commit
a95f9c1aa5
  1. 56
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts
  2. 16
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-fee-and-dividend.spec.ts
  3. 10
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
  4. 90
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

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

@ -162,7 +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'),
max: new Big('41.72313811883729606471'), max: new Big('41.65910103572163194783'),
ytd: new Big('-0.04016229506406263535') ytd: new Big('-0.04016229506406263535')
}, },
netPerformanceWithCurrencyEffectMap: { netPerformanceWithCurrencyEffectMap: {
@ -261,8 +261,8 @@ describe('PortfolioCalculator', () => {
max: { max: {
date: '2018-01-01', date: '2018-01-01',
netPerformance: 26458.9121202, netPerformance: 26458.9121202,
netPerformanceInPercentage: 42.50435329547954, netPerformanceInPercentage: 42.43911719562077,
netPerformanceInPercentageWithCurrencyEffect: 41.72313811883715, netPerformanceInPercentageWithCurrencyEffect: 41.65910103572173,
netPerformanceWithCurrencyEffect: 26516.2087014, netPerformanceWithCurrencyEffect: 26516.2087014,
totalInvestmentValueWithCurrencyEffect: 318.54266729999995, totalInvestmentValueWithCurrencyEffect: 318.54266729999995,
valueWithCurrencyEffect: 13298.425356 valueWithCurrencyEffect: 13298.425356
@ -278,5 +278,55 @@ describe('PortfolioCalculator', () => {
} }
}); });
}); });
it.only('with BTCUSD buy and sell partially and fewer chart items', async () => {
jest.useFakeTimers().setSystemTime(parseDate('2018-01-01').getTime());
const environmentConfigurationService = new ConfigurationService();
// Fewer chart items give fewer chart dates, which must not change the
// average investment
jest.spyOn(configurationService, 'get').mockImplementation((key) => {
return key === 'MAX_CHART_ITEMS'
? 50
: environmentConfigurationService.get(key);
});
const { activities, userCurrency } = loadActivitiesFromExportFile(
'btcusd-buy-and-sell-partially.json'
);
const portfolioCalculator = portfolioCalculatorFactory.createCalculator({
activities,
calculationType: PerformanceCalculationType.ROAI,
currency: userCurrency,
usePortfolioSnapshotCache: false,
userId: userDummyData.id
});
const portfolioSnapshot = await portfolioCalculator.computeSnapshot();
const performanceByDateRange = await getPerformanceByDateRange({
portfolioCalculator,
dateRanges: ['max']
});
expect(
portfolioSnapshot.positions[0]
.netPerformancePercentageWithCurrencyEffectMap
).toMatchObject({
max: new Big('41.65910103572163194783')
});
// Other weights give another rounding of the floating point numbers
expect(performanceByDateRange.max.netPerformanceInPercentage).toBeCloseTo(
42.43911719562077,
10
);
expect(
performanceByDateRange.max.netPerformanceInPercentageWithCurrencyEffect
).toBeCloseTo(41.65910103572173, 10);
});
}); });
}); });

16
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-fee-and-dividend.spec.ts

@ -190,9 +190,9 @@ describe('PortfolioCalculator', () => {
expect(performanceByDateRange).toMatchObject({ expect(performanceByDateRange).toMatchObject({
max: { max: {
dividendInBaseCurrency: 5.62, dividendInBaseCurrency: 5.62,
dividendInPercentageWithCurrencyEffect: 0.01882242615044535, dividendInPercentageWithCurrencyEffect: 0.01882242615044543,
netPerformance: 18.87, netPerformance: 18.87,
netPerformanceInPercentage: 0.06319914260834586 netPerformanceInPercentage: 0.06319914260834614
} }
}); });
}); });
@ -282,9 +282,9 @@ describe('PortfolioCalculator', () => {
expect(performanceByDateRange).toMatchObject({ expect(performanceByDateRange).toMatchObject({
max: { max: {
dividendInBaseCurrency: 5.62, dividendInBaseCurrency: 5.62,
dividendInPercentageWithCurrencyEffect: 0.01882242615044535, dividendInPercentageWithCurrencyEffect: 0.01882242615044543,
netPerformance: 19.87, netPerformance: 19.87,
netPerformanceInPercentage: 0.06654832875611194 netPerformanceInPercentage: 0.06654832875611222
} }
}); });
}); });
@ -373,9 +373,9 @@ describe('PortfolioCalculator', () => {
expect(performanceByDateRange).toMatchObject({ expect(performanceByDateRange).toMatchObject({
max: { max: {
dividendInBaseCurrency: 0.62, dividendInBaseCurrency: 0.62,
dividendInPercentageWithCurrencyEffect: 0.002076495411614967, dividendInPercentageWithCurrencyEffect: 0.002076495411614976,
netPerformance: 14.87, netPerformance: 14.87,
netPerformanceInPercentage: 0.049802398017281556 netPerformanceInPercentage: 0.049802398017281764
} }
}); });
}); });
@ -458,11 +458,11 @@ describe('PortfolioCalculator', () => {
}, },
max: { max: {
dividendInBaseCurrency: 1.3, dividendInBaseCurrency: 1.3,
dividendInPercentageWithCurrencyEffect: 0.004353941992095899 dividendInPercentageWithCurrencyEffect: 0.004353941992095918
}, },
ytd: { ytd: {
dividendInBaseCurrency: 0.68, dividendInBaseCurrency: 0.68,
dividendInPercentageWithCurrencyEffect: 0.002002886512915673 dividendInPercentageWithCurrencyEffect: 0.002002886512915671
} }
}); });
}); });

10
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts

@ -177,10 +177,10 @@ describe('PortfolioCalculator', () => {
max: { max: {
date: '2023-07-10', date: '2023-07-10',
dividendInBaseCurrency: 0.62, dividendInBaseCurrency: 0.62,
dividendInPercentageWithCurrencyEffect: 0.002076495411614967, dividendInPercentageWithCurrencyEffect: 0.002076495411614976,
netPerformance: 14.87, netPerformance: 14.87,
netPerformanceInPercentage: 0.049802398017281556, netPerformanceInPercentage: 0.049802398017281764,
netPerformanceInPercentageWithCurrencyEffect: 0.049802398017281556, netPerformanceInPercentageWithCurrencyEffect: 0.049802398017281764,
netPerformanceWithCurrencyEffect: 14.87, netPerformanceWithCurrencyEffect: 14.87,
totalInvestmentValueWithCurrencyEffect: 298.58, totalInvestmentValueWithCurrencyEffect: 298.58,
valueWithCurrencyEffect: 331.83 valueWithCurrencyEffect: 331.83
@ -190,8 +190,8 @@ describe('PortfolioCalculator', () => {
dividendInBaseCurrency: 0, dividendInBaseCurrency: 0,
dividendInPercentageWithCurrencyEffect: 0, dividendInPercentageWithCurrencyEffect: 0,
netPerformance: -7.6800000000000015, netPerformance: -7.6800000000000015,
netPerformanceInPercentage: -0.022620835910577015, netPerformanceInPercentage: -0.022620835910576995,
netPerformanceInPercentageWithCurrencyEffect: -0.022620835910577015, netPerformanceInPercentageWithCurrencyEffect: -0.022620835910576995,
netPerformanceWithCurrencyEffect: -7.6800000000000015, netPerformanceWithCurrencyEffect: -7.6800000000000015,
totalInvestmentValueWithCurrencyEffect: 298.58, totalInvestmentValueWithCurrencyEffect: 298.58,
valueWithCurrencyEffect: 331.83 valueWithCurrencyEffect: 331.83

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

@ -29,7 +29,6 @@ import {
isBefore, isBefore,
isThisYear isThisYear
} from 'date-fns'; } from 'date-fns';
import { sum } from 'lodash-es';
export class RoaiPortfolioCalculator extends PortfolioCalculator { export class RoaiPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance( protected calculateOverallPerformance(
@ -210,10 +209,12 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}: { }: {
historicalDataItems: HistoricalDataItem[]; historicalDataItems: HistoricalDataItem[];
}): { [date: string]: PerformancePercentages } { }): { [date: string]: PerformancePercentages } {
const averageInvestmentValues: number[] = [];
const averageInvestmentValuesWithCurrencyEffect: number[] = [];
let grossPerformanceAtStartDate: number; let grossPerformanceAtStartDate: number;
let grossPerformanceWithCurrencyEffectAtStartDate: number; let grossPerformanceWithCurrencyEffectAtStartDate: number;
let sumOfWeightedInvestments = 0;
let sumOfWeightedInvestmentsWithCurrencyEffect = 0;
let totalInvestmentDays = 0;
let totalInvestmentDaysWithCurrencyEffect = 0;
const performancePercentagesByDate: { const performancePercentagesByDate: {
[date: string]: PerformancePercentages; [date: string]: PerformancePercentages;
@ -229,33 +230,61 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
grossPerformanceWithCurrencyEffectAtStartDate = grossPerformanceWithCurrencyEffectAtStartDate =
historicalDataItem.valueWithCurrencyEffect - historicalDataItem.valueWithCurrencyEffect -
historicalDataItem.totalInvestmentValueWithCurrencyEffect; historicalDataItem.totalInvestmentValueWithCurrencyEffect;
} } else {
// The chart dates are not evenly spaced, and the investment changes on
// Add the gross performance at the start date of the range to the // a chart date only. Thus the investment of the previous chart date
// investment of each day. Thus the range starts with the value of its // applies to each day until this chart date.
// first day, and subsequent buy and sell activities stay included. const previousHistoricalDataItem = historicalDataItems[index - 1];
if (historicalDataItem.totalInvestment > 0) {
averageInvestmentValues.push( const daysSincePreviousChartDate = differenceInDays(
historicalDataItem.totalInvestment + grossPerformanceAtStartDate parseDate(historicalDataItem.date),
parseDate(previousHistoricalDataItem.date)
); );
if (previousHistoricalDataItem.totalInvestment > 0) {
sumOfWeightedInvestments +=
(previousHistoricalDataItem.totalInvestment +
grossPerformanceAtStartDate) *
daysSincePreviousChartDate;
totalInvestmentDays += daysSincePreviousChartDate;
} }
if (historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0) { if (
averageInvestmentValuesWithCurrencyEffect.push( previousHistoricalDataItem.totalInvestmentValueWithCurrencyEffect > 0
historicalDataItem.totalInvestmentValueWithCurrencyEffect + ) {
grossPerformanceWithCurrencyEffectAtStartDate sumOfWeightedInvestmentsWithCurrencyEffect +=
); (previousHistoricalDataItem.totalInvestmentValueWithCurrencyEffect +
grossPerformanceWithCurrencyEffectAtStartDate) *
daysSincePreviousChartDate;
totalInvestmentDaysWithCurrencyEffect += daysSincePreviousChartDate;
}
} }
// 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. The
// investment of this chart date counts for this day only.
const averageInvestmentValue = const averageInvestmentValue =
averageInvestmentValues.length > 0 historicalDataItem.totalInvestment > 0
? sum(averageInvestmentValues) / averageInvestmentValues.length ? (sumOfWeightedInvestments +
(historicalDataItem.totalInvestment +
grossPerformanceAtStartDate)) /
(totalInvestmentDays + 1)
: totalInvestmentDays > 0
? sumOfWeightedInvestments / totalInvestmentDays
: 0; : 0;
const averageInvestmentValueWithCurrencyEffect = const averageInvestmentValueWithCurrencyEffect =
averageInvestmentValuesWithCurrencyEffect.length > 0 historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0
? sum(averageInvestmentValuesWithCurrencyEffect) / ? (sumOfWeightedInvestmentsWithCurrencyEffect +
averageInvestmentValuesWithCurrencyEffect.length (historicalDataItem.totalInvestmentValueWithCurrencyEffect +
grossPerformanceWithCurrencyEffectAtStartDate)) /
(totalInvestmentDaysWithCurrencyEffect + 1)
: totalInvestmentDaysWithCurrencyEffect > 0
? sumOfWeightedInvestmentsWithCurrencyEffect /
totalInvestmentDaysWithCurrencyEffect
: 0; : 0;
performancePercentagesByDate[historicalDataItem.date] = { performancePercentagesByDate[historicalDataItem.date] = {
@ -632,6 +661,7 @@ 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];
@ -642,17 +672,27 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
break; break;
} }
// 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 daysUntilNextChartDate = nextChartDateString
? differenceInDays(parseDate(nextChartDateString), parseDate(date))
: 1;
nextChartDateString = date;
if ( if (
investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big &&
investmentValuesAccumulatedWithCurrencyEffect[date].gt(0) investmentValuesAccumulatedWithCurrencyEffect[date].gt(0)
) { ) {
average = average.add( average = average.add(
investmentValuesAccumulatedWithCurrencyEffect[date].add( investmentValuesAccumulatedWithCurrencyEffect[date]
grossPerformanceAtDateRangeStartWithCurrencyEffect .add(grossPerformanceAtDateRangeStartWithCurrencyEffect)
) .mul(daysUntilNextChartDate)
); );
dayCount++; dayCount += daysUntilNextChartDate;
} }
} }

Loading…
Cancel
Save