diff --git a/CHANGELOG.md b/CHANGELOG.md index fe920851eb..603da63dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the visibility of the asset class, asset sub class, fee and quantity fields of a valuable in the create activity dialog - Fixed the missing mapping for Aland Islands in the country weightings of the _Financial Modeling Prep_ service +- Fixed the net performance percentage of date ranges in the portfolio performance calculation by weighting the average investment by the number of days between the chart dates ## 3.80.2 - 2026-10-06 diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 9b94545fd8..1154f897d1 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -362,6 +362,18 @@ 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 parsedChartDates = chartDates.map((chartDate) => { + return parseDate(chartDate); + }); + + const daysUntilNextChartDate = parsedChartDates + .slice(1) + .map((nextChartDate, index) => { + return differenceInDays(nextChartDate, parsedChartDates[index]); + }); + const errors: ResponseError['errors'] = []; let hasAnyHoldingPerformanceErrors = false; @@ -438,6 +450,7 @@ export abstract class PortfolioCalculator { totalLiabilitiesInBaseCurrency } = this.getHoldingPerformance({ chartDates, + daysUntilNextChartDate, marketSymbolMap, dataSource: item.dataSource, end: this.endDate, @@ -935,6 +948,7 @@ export abstract class PortfolioCalculator { protected abstract getHoldingPerformance({ chartDates, dataSource, + daysUntilNextChartDate, end, exchangeRates, marketSymbolMap, @@ -942,6 +956,7 @@ export abstract class PortfolioCalculator { symbol }: { chartDates: string[]; + daysUntilNextChartDate: 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 15474a0236..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,7 +162,8 @@ describe('PortfolioCalculator', () => { netPerformancePercentage: new Big('42.41978276196153750666'), netPerformancePercentageWithCurrencyEffectMap: { '1d': new Big('-0.04016229506406263535'), - max: new Big('41.72313811883729606471'), + '2017': new Big('43.14843430282283692638'), + max: new Big('41.65910103572163194783'), ytd: new Big('-0.04016229506406263535') }, netPerformanceWithCurrencyEffectMap: { @@ -258,11 +259,20 @@ 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, - netPerformanceInPercentage: 42.50435329547954, - netPerformanceInPercentageWithCurrencyEffect: 41.72313811883715, + netPerformanceInPercentage: 42.43911719562077, + netPerformanceInPercentageWithCurrencyEffect: 41.65910103572173, netPerformanceWithCurrencyEffect: 26516.2087014, totalInvestmentValueWithCurrencyEffect: 318.54266729999995, valueWithCurrencyEffect: 13298.425356 @@ -278,5 +288,66 @@ 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. 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 + : 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: ['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 + ); + + expect( + performanceByDateRange.max.netPerformanceInPercentageWithCurrencyEffect + ).toBeCloseTo(41.65910103572173, 10); + }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-fee-and-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-fee-and-dividend.spec.ts index 18929a680c..bb6385d18d 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-fee-and-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-fee-and-dividend.spec.ts @@ -190,9 +190,12 @@ describe('PortfolioCalculator', () => { expect(performanceByDateRange).toMatchObject({ max: { dividendInBaseCurrency: 5.62, - dividendInPercentageWithCurrencyEffect: 0.01882242615044535, + dividendInPercentageWithCurrencyEffect: expect.closeTo( + 0.01882242615044535, + 10 + ), netPerformance: 18.87, - netPerformanceInPercentage: 0.06319914260834586 + netPerformanceInPercentage: expect.closeTo(0.06319914260834586, 10) } }); }); @@ -282,9 +285,12 @@ describe('PortfolioCalculator', () => { expect(performanceByDateRange).toMatchObject({ max: { dividendInBaseCurrency: 5.62, - dividendInPercentageWithCurrencyEffect: 0.01882242615044535, + dividendInPercentageWithCurrencyEffect: expect.closeTo( + 0.01882242615044535, + 10 + ), netPerformance: 19.87, - netPerformanceInPercentage: 0.06654832875611194 + netPerformanceInPercentage: expect.closeTo(0.06654832875611194, 10) } }); }); @@ -373,9 +379,12 @@ describe('PortfolioCalculator', () => { expect(performanceByDateRange).toMatchObject({ max: { dividendInBaseCurrency: 0.62, - dividendInPercentageWithCurrencyEffect: 0.002076495411614967, + dividendInPercentageWithCurrencyEffect: expect.closeTo( + 0.002076495411614967, + 10 + ), netPerformance: 14.87, - netPerformanceInPercentage: 0.049802398017281556 + netPerformanceInPercentage: expect.closeTo(0.049802398017281556, 10) } }); }); @@ -458,11 +467,17 @@ describe('PortfolioCalculator', () => { }, max: { dividendInBaseCurrency: 1.3, - dividendInPercentageWithCurrencyEffect: 0.004353941992095899 + dividendInPercentageWithCurrencyEffect: expect.closeTo( + 0.004353941992095899, + 10 + ) }, ytd: { dividendInBaseCurrency: 0.68, - dividendInPercentageWithCurrencyEffect: 0.002002886512915673 + dividendInPercentageWithCurrencyEffect: expect.closeTo( + 0.002002886512915673, + 10 + ) } }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts index 0fdd0b3000..2b89ddd31b 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -177,10 +177,16 @@ describe('PortfolioCalculator', () => { max: { date: '2023-07-10', dividendInBaseCurrency: 0.62, - dividendInPercentageWithCurrencyEffect: 0.002076495411614967, + dividendInPercentageWithCurrencyEffect: expect.closeTo( + 0.002076495411614967, + 10 + ), netPerformance: 14.87, - netPerformanceInPercentage: 0.049802398017281556, - netPerformanceInPercentageWithCurrencyEffect: 0.049802398017281556, + netPerformanceInPercentage: expect.closeTo(0.049802398017281556, 10), + netPerformanceInPercentageWithCurrencyEffect: expect.closeTo( + 0.049802398017281556, + 10 + ), netPerformanceWithCurrencyEffect: 14.87, totalInvestmentValueWithCurrencyEffect: 298.58, valueWithCurrencyEffect: 331.83 @@ -190,8 +196,11 @@ describe('PortfolioCalculator', () => { dividendInBaseCurrency: 0, dividendInPercentageWithCurrencyEffect: 0, netPerformance: -7.6800000000000015, - netPerformanceInPercentage: -0.022620835910577015, - netPerformanceInPercentageWithCurrencyEffect: -0.022620835910577015, + netPerformanceInPercentage: expect.closeTo(-0.022620835910577015, 10), + netPerformanceInPercentageWithCurrencyEffect: expect.closeTo( + -0.022620835910577015, + 10 + ), netPerformanceWithCurrencyEffect: -7.6800000000000015, totalInvestmentValueWithCurrencyEffect: 298.58, valueWithCurrencyEffect: 331.83 diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts index 99bee2c21d..5f8d9cdc44 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts @@ -1,3 +1,186 @@ +import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; +import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; +import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; +import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; +import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', + () => { + return { + PortfolioSnapshotService: jest.fn().mockImplementation(() => { + return PortfolioSnapshotServiceMock; + }) + }; + } +); + +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + +function getHistoricalDataItem({ + date, + investment, + netPerformance +}: { + date: string; + investment: number; + netPerformance: number; +}): HistoricalDataItem { + return { + date, + netPerformance, + dividendInBaseCurrency: 0, + netPerformanceWithCurrencyEffect: netPerformance, + totalInvestment: investment, + totalInvestmentValueWithCurrencyEffect: investment, + value: investment + netPerformance, + valueWithCurrencyEffect: investment + netPerformance + }; +} + describe('PortfolioCalculator', () => { - test.skip('Skip empty test', () => 1); + let configurationService: ConfigurationService; + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let portfolioCalculator: PortfolioCalculator; + let portfolioCalculatorFactory: PortfolioCalculatorFactory; + let portfolioSnapshotService: PortfolioSnapshotService; + let redisCacheService: RedisCacheService; + + beforeEach(() => { + PortfolioSnapshotServiceMock.reset(); + RedisCacheServiceMock.reset(); + + configurationService = new ConfigurationService(); + + currentRateService = new CurrentRateService(null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + portfolioSnapshotService = new PortfolioSnapshotService(null, null); + + redisCacheService = new RedisCacheService(null, null); + + portfolioCalculatorFactory = new PortfolioCalculatorFactory( + configurationService, + currentRateService, + exchangeRateDataService, + portfolioSnapshotService, + redisCacheService + ); + + portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities: [], + calculationType: PerformanceCalculationType.ROAI, + currency: 'USD', + userId: userDummyData.id + }); + }); + + describe('calculate performance percentages for date range', () => { + it('with a buy followed by a gap between chart dates', () => { + const performancePercentagesByDate = portfolioCalculator[ + 'calculatePerformancePercentagesForDateRange' + ]({ + historicalDataItems: [ + getHistoricalDataItem({ + date: '2017-01-01', + investment: 100, + netPerformance: 0 + }), + // Buy, followed by a gap of 21 days until the next chart date + getHistoricalDataItem({ + date: '2017-01-02', + investment: 200, + netPerformance: 0 + }), + // Sell + getHistoricalDataItem({ + date: '2017-01-23', + investment: 100, + netPerformance: 44 + }) + ] + }); + + // The investment of each chart date applies to each day until the next + // chart date. The investment of the last chart date counts for this day + // only: (1 * 100 + 21 * 200 + 1 * 100) / 23 = 4400 / 23 + expect( + performancePercentagesByDate['2017-01-23'].netPerformanceInPercentage + ).toBeCloseTo(0.23, 10); + + expect( + performancePercentagesByDate['2017-01-23'] + .netPerformanceInPercentageWithCurrencyEffect + ).toBeCloseTo(0.23, 10); + }); + + it('with an additional chart date of the same investment', () => { + const performancePercentagesByDate = portfolioCalculator[ + 'calculatePerformancePercentagesForDateRange' + ]({ + historicalDataItems: [ + getHistoricalDataItem({ + date: '2017-01-01', + investment: 100, + netPerformance: 0 + }), + getHistoricalDataItem({ + date: '2017-01-02', + investment: 200, + netPerformance: 0 + }), + // An additional chart date splits the gap but must not change the + // average investment + getHistoricalDataItem({ + date: '2017-01-12', + investment: 200, + netPerformance: 20 + }), + getHistoricalDataItem({ + date: '2017-01-23', + investment: 100, + netPerformance: 44 + }) + ] + }); + + expect( + performancePercentagesByDate['2017-01-23'].netPerformanceInPercentage + ).toBeCloseTo(0.23, 10); + + expect( + performancePercentagesByDate['2017-01-23'] + .netPerformanceInPercentageWithCurrencyEffect + ).toBeCloseTo(0.23, 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 a1e41b53a2..0115b4a57c 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -29,7 +29,6 @@ import { isBefore, isThisYear } from 'date-fns'; -import { sum } from 'lodash-es'; export class RoaiPortfolioCalculator extends PortfolioCalculator { protected calculateOverallPerformance( @@ -210,16 +209,21 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { }: { historicalDataItems: HistoricalDataItem[]; }): { [date: string]: PerformancePercentages } { - const averageInvestmentValues: number[] = []; - const averageInvestmentValuesWithCurrencyEffect: number[] = []; 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) { @@ -229,34 +233,53 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { 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); + + if (previousHistoricalDataItem.totalInvestment > 0) { + sumOfWeightedInvestments += + (previousHistoricalDataItem.totalInvestment + + grossPerformanceAtStartDate) * + daysSincePreviousChartDate; + + totalInvestmentDays += daysSincePreviousChartDate; + } + + if ( + previousHistoricalDataItem.totalInvestmentValueWithCurrencyEffect > 0 + ) { + 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. - if (historicalDataItem.totalInvestment > 0) { - averageInvestmentValues.push( - historicalDataItem.totalInvestment + grossPerformanceAtStartDate - ); - } - - if (historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0) { - averageInvestmentValuesWithCurrencyEffect.push( - historicalDataItem.totalInvestmentValueWithCurrencyEffect + - grossPerformanceWithCurrencyEffectAtStartDate - ); - } - - const averageInvestmentValue = - averageInvestmentValues.length > 0 - ? sum(averageInvestmentValues) / averageInvestmentValues.length - : 0; + const averageInvestmentValue = this.getAverageInvestment({ + grossPerformanceAtStartDate, + sumOfWeightedInvestments, + totalInvestmentDays, + investment: historicalDataItem.totalInvestment + }); const averageInvestmentValueWithCurrencyEffect = - averageInvestmentValuesWithCurrencyEffect.length > 0 - ? sum(averageInvestmentValuesWithCurrencyEffect) / - averageInvestmentValuesWithCurrencyEffect.length - : 0; + this.getAverageInvestment({ + grossPerformanceAtStartDate: + grossPerformanceWithCurrencyEffectAtStartDate, + investment: historicalDataItem.totalInvestmentValueWithCurrencyEffect, + sumOfWeightedInvestments: sumOfWeightedInvestmentsWithCurrencyEffect, + totalInvestmentDays: totalInvestmentDaysWithCurrencyEffect + }); performancePercentagesByDate[historicalDataItem.date] = { dividendInPercentageWithCurrencyEffect: @@ -274,6 +297,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { averageInvestmentValueWithCurrencyEffect : 0 }; + + previousDate = date; } return performancePercentagesByDate; @@ -282,6 +307,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { protected getHoldingPerformance({ chartDates, dataSource, + daysUntilNextChartDate, end, exchangeRates, marketSymbolMap, @@ -289,6 +315,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { symbol }: { chartDates: string[]; + daysUntilNextChartDate: number[]; end: Date; exchangeRates: { [dateString: string]: number }; marketSymbolMap: { @@ -642,17 +669,28 @@ 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 investmentDays = isLastChartDateOfRange + ? 1 + : daysUntilNextChartDate[i]; + if ( investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && investmentValuesAccumulatedWithCurrencyEffect[date].gt(0) ) { average = average.add( - investmentValuesAccumulatedWithCurrencyEffect[date].add( - grossPerformanceAtDateRangeStartWithCurrencyEffect - ) + investmentValuesAccumulatedWithCurrencyEffect[date] + .add(grossPerformanceAtDateRangeStartWithCurrencyEffect) + .mul(investmentDays) ); - dayCount++; + dayCount += investmentDays; } } @@ -753,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; + } }