diff --git a/CHANGELOG.md b/CHANGELOG.md index 14bdfe11a4..0fb7e1e93f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- Fixed the net performance percentage of date ranges in the portfolio performance calculation by including the gross performance at the start date + ## 3.71.0 - 2026-09-19 ### Added diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index dfbe99c8de..d73ea60161 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -794,15 +794,25 @@ export abstract class PortfolioCalculator { const chart: HistoricalDataItem[] = []; + let grossPerformanceAtStartDate: number; + let grossPerformanceWithCurrencyEffectAtStartDate: number; let netPerformanceAtStartDate: number; let netPerformanceWithCurrencyEffectAtStartDate: number; - const totalInvestmentValuesWithCurrencyEffect: number[] = []; + const timeWeightedInvestmentValues: number[] = []; + const timeWeightedInvestmentValuesWithCurrencyEffect: number[] = []; for (const historicalDataItem of historicalData) { const date = resetHours(parseDate(historicalDataItem.date)); if (!isBefore(date, start) && !isAfter(date, end)) { if (!isNumber(netPerformanceAtStartDate)) { + grossPerformanceAtStartDate = + historicalDataItem.value - historicalDataItem.totalInvestment; + + grossPerformanceWithCurrencyEffectAtStartDate = + historicalDataItem.valueWithCurrencyEffect - + historicalDataItem.totalInvestmentValueWithCurrencyEffect; + netPerformanceAtStartDate = historicalDataItem.netPerformance; netPerformanceWithCurrencyEffectAtStartDate = @@ -816,33 +826,48 @@ export abstract class PortfolioCalculator { historicalDataItem.netPerformanceWithCurrencyEffect - netPerformanceWithCurrencyEffectAtStartDate; + // 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) { + timeWeightedInvestmentValues.push( + historicalDataItem.totalInvestment + grossPerformanceAtStartDate + ); + } + if (historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0) { - totalInvestmentValuesWithCurrencyEffect.push( - historicalDataItem.totalInvestmentValueWithCurrencyEffect + timeWeightedInvestmentValuesWithCurrencyEffect.push( + historicalDataItem.totalInvestmentValueWithCurrencyEffect + + grossPerformanceWithCurrencyEffectAtStartDate ); } const timeWeightedInvestmentValue = - totalInvestmentValuesWithCurrencyEffect.length > 0 - ? sum(totalInvestmentValuesWithCurrencyEffect) / - totalInvestmentValuesWithCurrencyEffect.length + timeWeightedInvestmentValues.length > 0 + ? sum(timeWeightedInvestmentValues) / + timeWeightedInvestmentValues.length + : 0; + + const timeWeightedInvestmentValueWithCurrencyEffect = + timeWeightedInvestmentValuesWithCurrencyEffect.length > 0 + ? sum(timeWeightedInvestmentValuesWithCurrencyEffect) / + timeWeightedInvestmentValuesWithCurrencyEffect.length : 0; chart.push({ ...historicalDataItem, - netPerformance: - historicalDataItem.netPerformance - netPerformanceAtStartDate, + netPerformance: netPerformanceSinceStartDate, netPerformanceWithCurrencyEffect: netPerformanceWithCurrencyEffectSinceStartDate, netPerformanceInPercentage: - timeWeightedInvestmentValue === 0 - ? 0 - : netPerformanceSinceStartDate / timeWeightedInvestmentValue, + timeWeightedInvestmentValue > 0 + ? netPerformanceSinceStartDate / timeWeightedInvestmentValue + : 0, netPerformanceInPercentageWithCurrencyEffect: - timeWeightedInvestmentValue === 0 - ? 0 - : netPerformanceWithCurrencyEffectSinceStartDate / - timeWeightedInvestmentValue + timeWeightedInvestmentValueWithCurrencyEffect > 0 + ? netPerformanceWithCurrencyEffectSinceStartDate / + timeWeightedInvestmentValueWithCurrencyEffect + : 0 }); } } 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 e54fd96aa7..237c1ae220 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 @@ -190,10 +190,14 @@ describe('PortfolioCalculator', () => { netPerformance: new Big('27172.74').mul(0.97373), netPerformancePercentage: new Big('42.41978276196153750666'), netPerformancePercentageWithCurrencyEffectMap: { - max: new Big('41.72313811883729606471') + '1d': new Big('-0.04016229506406263535'), + max: new Big('41.72313811883729606471'), + ytd: new Big('-0.04016229506406263535') }, netPerformanceWithCurrencyEffectMap: { - max: new Big('26516.208701400000064086') + '1d': new Big('-556.443324'), + max: new Big('26516.208701400000064086'), + ytd: new Big('-556.443324') }, quantity: new Big('1'), symbol: 'BTCUSD', @@ -281,8 +285,8 @@ describe('PortfolioCalculator', () => { '1d': { date: '2018-01-01', netPerformance: -486.0860160000011, - netPerformanceInPercentage: -1.5259683109961866, - netPerformanceInPercentageWithCurrencyEffect: -1.7468407881319952, + netPerformanceInPercentage: -0.035263202509112565, + netPerformanceInPercentageWithCurrencyEffect: -0.040162295064062624, netPerformanceWithCurrencyEffect: -556.4433239999998, totalInvestmentValueWithCurrencyEffect: 318.54266729999995, valueWithCurrencyEffect: 13298.425356 @@ -290,7 +294,7 @@ describe('PortfolioCalculator', () => { max: { date: '2018-01-01', netPerformance: 26458.9121202, - netPerformanceInPercentage: 41.63298219956282, + netPerformanceInPercentage: 42.50435329547954, netPerformanceInPercentageWithCurrencyEffect: 41.72313811883715, netPerformanceWithCurrencyEffect: 26516.2087014, totalInvestmentValueWithCurrencyEffect: 318.54266729999995, @@ -299,8 +303,8 @@ describe('PortfolioCalculator', () => { ytd: { date: '2018-01-01', netPerformance: -486.0860160000011, - netPerformanceInPercentage: -1.5259683109961866, - netPerformanceInPercentageWithCurrencyEffect: -1.7468407881319952, + netPerformanceInPercentage: -0.035263202509112565, + netPerformanceInPercentageWithCurrencyEffect: -0.040162295064062624, netPerformanceWithCurrencyEffect: -556.4433239999998, totalInvestmentValueWithCurrencyEffect: 318.54266729999995, valueWithCurrencyEffect: 13298.425356 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 bb16af5ef6..87e13e75c9 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 @@ -193,8 +193,8 @@ describe('PortfolioCalculator', () => { '1d': { date: '2023-07-10', netPerformance: -5.390000000000001, - netPerformanceInPercentage: -0.018052113336459244, - netPerformanceInPercentageWithCurrencyEffect: -0.018052113336459244, + netPerformanceInPercentage: -0.015983630864124312, + netPerformanceInPercentageWithCurrencyEffect: -0.015983630864124312, netPerformanceWithCurrencyEffect: -5.390000000000001, totalInvestmentValueWithCurrencyEffect: 298.58, valueWithCurrencyEffect: 331.83 @@ -211,8 +211,8 @@ describe('PortfolioCalculator', () => { ytd: { date: '2023-07-10', netPerformance: -7.68, - netPerformanceInPercentage: -0.025721749614843515, - netPerformanceInPercentageWithCurrencyEffect: -0.025721749614843515, + netPerformanceInPercentage: -0.022620835910577012, + netPerformanceInPercentageWithCurrencyEffect: -0.022620835910577012, netPerformanceWithCurrencyEffect: -7.68, totalInvestmentValueWithCurrencyEffect: 298.58, valueWithCurrencyEffect: 331.83