diff --git a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts index f83b175128..2572c70598 100644 --- a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts @@ -24,6 +24,7 @@ export class MwrPortfolioCalculator extends PortfolioCalculator { protected getHoldingPerformance({}: { chartDates: string[]; + daysUntilNextChartDate: { [date: string]: number }; end: Date; exchangeRates: { [dateString: string]: number }; marketSymbolMap: { diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 9b94545fd8..a9db3ba429 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -362,6 +362,17 @@ export abstract class PortfolioCalculator { 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'] = []; let hasAnyHoldingPerformanceErrors = false; @@ -438,6 +449,7 @@ export abstract class PortfolioCalculator { totalLiabilitiesInBaseCurrency } = this.getHoldingPerformance({ chartDates, + daysUntilNextChartDate, marketSymbolMap, dataSource: item.dataSource, end: this.endDate, @@ -935,6 +947,7 @@ export abstract class PortfolioCalculator { protected abstract getHoldingPerformance({ chartDates, dataSource, + daysUntilNextChartDate, end, exchangeRates, marketSymbolMap, @@ -942,6 +955,7 @@ export abstract class PortfolioCalculator { symbol }: { chartDates: string[]; + daysUntilNextChartDate: { [date: string]: number }; end: Date; exchangeRates: { [dateString: string]: number }; marketSymbolMap: { diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 11776d14cc..0110f68f27 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/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({ portfolioCalculator, - dateRanges: ['1d', 'max', 'ytd'] + dateRanges: ['1d', '2017', 'max', 'ytd'] }); const investments = portfolioCalculator.getInvestments(); @@ -162,6 +162,7 @@ describe('PortfolioCalculator', () => { netPerformancePercentage: new Big('42.41978276196153750666'), netPerformancePercentageWithCurrencyEffectMap: { '1d': new Big('-0.04016229506406263535'), + '2017': new Big('43.14843430282283692638'), max: new Big('41.65910103572163194783'), ytd: new Big('-0.04016229506406263535') }, @@ -258,6 +259,15 @@ describe('PortfolioCalculator', () => { totalInvestmentValueWithCurrencyEffect: 318.54266729999995, 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: { date: '2018-01-01', netPerformance: 26458.9121202, @@ -285,7 +295,8 @@ describe('PortfolioCalculator', () => { const environmentConfigurationService = new ConfigurationService(); // 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) => { return key === 'MAX_CHART_ITEMS' ? 50 @@ -308,17 +319,27 @@ describe('PortfolioCalculator', () => { const performanceByDateRange = await getPerformanceByDateRange({ portfolioCalculator, - dateRanges: ['max'] + dateRanges: ['2017', 'max'] }); expect( portfolioSnapshot.positions[0] .netPerformancePercentageWithCurrencyEffectMap ).toMatchObject({ + '2017': new Big('43.14843430282283692638'), max: new Big('41.65910103572163194783') }); // 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( 42.43911719562077, 10 diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts index 6d8e9d15fd..cd75355f8b 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -211,6 +211,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { }): { [date: string]: PerformancePercentages } { let grossPerformanceAtStartDate: number; let grossPerformanceWithCurrencyEffectAtStartDate: number; + let previousDate: Date; let sumOfWeightedInvestments = 0; let sumOfWeightedInvestmentsWithCurrencyEffect = 0; let totalInvestmentDays = 0; @@ -221,6 +222,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } = {}; 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) { @@ -236,10 +239,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { // applies to each day until this chart date. const previousHistoricalDataItem = historicalDataItems[index - 1]; - const daysSincePreviousChartDate = differenceInDays( - parseDate(historicalDataItem.date), - parseDate(previousHistoricalDataItem.date) - ); + const daysSincePreviousChartDate = differenceInDays(date, previousDate); if (previousHistoricalDataItem.totalInvestment > 0) { sumOfWeightedInvestments += @@ -264,28 +264,22 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { // 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 = - historicalDataItem.totalInvestment > 0 - ? (sumOfWeightedInvestments + - (historicalDataItem.totalInvestment + - grossPerformanceAtStartDate)) / - (totalInvestmentDays + 1) - : totalInvestmentDays > 0 - ? sumOfWeightedInvestments / totalInvestmentDays - : 0; + // first day, and subsequent buy and sell activities stay included. + const averageInvestmentValue = this.getAverageInvestment({ + grossPerformanceAtStartDate, + sumOfWeightedInvestments, + totalInvestmentDays, + investment: historicalDataItem.totalInvestment + }); const averageInvestmentValueWithCurrencyEffect = - historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0 - ? (sumOfWeightedInvestmentsWithCurrencyEffect + - (historicalDataItem.totalInvestmentValueWithCurrencyEffect + - grossPerformanceWithCurrencyEffectAtStartDate)) / - (totalInvestmentDaysWithCurrencyEffect + 1) - : totalInvestmentDaysWithCurrencyEffect > 0 - ? sumOfWeightedInvestmentsWithCurrencyEffect / - totalInvestmentDaysWithCurrencyEffect - : 0; + this.getAverageInvestment({ + grossPerformanceAtStartDate: + grossPerformanceWithCurrencyEffectAtStartDate, + investment: historicalDataItem.totalInvestmentValueWithCurrencyEffect, + sumOfWeightedInvestments: sumOfWeightedInvestmentsWithCurrencyEffect, + totalInvestmentDays: totalInvestmentDaysWithCurrencyEffect + }); performancePercentagesByDate[historicalDataItem.date] = { dividendInPercentageWithCurrencyEffect: @@ -303,6 +297,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { averageInvestmentValueWithCurrencyEffect : 0 }; + + previousDate = date; } return performancePercentagesByDate; @@ -311,6 +307,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { protected getHoldingPerformance({ chartDates, dataSource, + daysUntilNextChartDate, end, exchangeRates, marketSymbolMap, @@ -318,6 +315,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { symbol }: { chartDates: string[]; + daysUntilNextChartDate: { [date: string]: number }; end: Date; exchangeRates: { [dateString: string]: number }; marketSymbolMap: { @@ -661,7 +659,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { let average = new Big(0); let dayCount = 0; - let nextChartDateString: string; for (let i = chartDates.length - 1; i >= 0; i -= 1) { const date = chartDates[i]; @@ -672,15 +669,16 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { 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 daysUntilNextChartDate = nextChartDateString - ? differenceInDays(parseDate(nextChartDateString), parseDate(date)) - : 1; - - nextChartDateString = date; + const investmentDays = isLastChartDateOfRange + ? 1 + : daysUntilNextChartDate[date]; if ( investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && @@ -689,10 +687,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { average = average.add( investmentValuesAccumulatedWithCurrencyEffect[date] .add(grossPerformanceAtDateRangeStartWithCurrencyEffect) - .mul(daysUntilNextChartDate) + .mul(investmentDays) ); - dayCount += daysUntilNextChartDate; + dayCount += investmentDays; } } @@ -793,4 +791,29 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { protected getPerformanceCalculationType() { 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; + } } diff --git a/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts index 862febf5db..9c12a4e970 100644 --- a/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts @@ -24,6 +24,7 @@ export class RoiPortfolioCalculator extends PortfolioCalculator { protected getHoldingPerformance({}: { chartDates: string[]; + daysUntilNextChartDate: { [date: string]: number }; end: Date; exchangeRates: { [dateString: string]: number }; marketSymbolMap: { diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index cad53c31ab..875b85194b 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -24,6 +24,7 @@ export class TwrPortfolioCalculator extends PortfolioCalculator { protected getHoldingPerformance({}: { chartDates: string[]; + daysUntilNextChartDate: { [date: string]: number }; end: Date; exchangeRates: { [dateString: string]: number }; marketSymbolMap: {