From c9237146e2951b69b61a40a14acda3ecc9bb3199 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Feb 2024 09:23:19 +0100 Subject: [PATCH] Feature/add investment value to chart (#2948) * Add investment value to chart * Update changelog --- CHANGELOG.md | 1 + .../timeline-specification.interface.ts | 6 - ...folio-calculator-baln-buy-and-sell.spec.ts | 13 +- .../portfolio-calculator-baln-buy.spec.ts | 13 +- ...ator-btcusd-buy-and-sell-partially.spec.ts | 83 +++---- .../portfolio-calculator-googl-buy.spec.ts | 36 ++- .../portfolio-calculator-no-orders.spec.ts | 10 +- ...ulator-novn-buy-and-sell-partially.spec.ts | 14 +- ...folio-calculator-novn-buy-and-sell.spec.ts | 18 +- .../src/app/portfolio/portfolio-calculator.ts | 215 +++++++----------- .../src/app/portfolio/portfolio.service.ts | 107 +++------ .../historical-data-item.interface.ts | 1 + libs/common/src/lib/interfaces/index.ts | 2 + .../interfaces/symbol-metrics.interface.ts | 5 +- 14 files changed, 245 insertions(+), 279 deletions(-) delete mode 100644 apps/api/src/app/portfolio/interfaces/timeline-specification.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b266db84..0cea3ec2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed an issue with the currency conversion in the investment timeline - Fixed the export in the lazy-loaded activities table on the portfolio activities page (experimental) ## 2.46.0 - 2024-01-28 diff --git a/apps/api/src/app/portfolio/interfaces/timeline-specification.interface.ts b/apps/api/src/app/portfolio/interfaces/timeline-specification.interface.ts deleted file mode 100644 index 1cd6510c7..000000000 --- a/apps/api/src/app/portfolio/interfaces/timeline-specification.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type Accuracy = 'day' | 'month' | 'year'; - -export interface TimelineSpecification { - accuracy: Accuracy; - start: string; -} diff --git a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts index a841311ba..110e8e30d 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -68,14 +68,20 @@ describe('PortfolioCalculator', () => { .spyOn(Date, 'now') .mockImplementation(() => parseDate('2021-12-18').getTime()); + const chartData = await portfolioCalculator.getChartData({ + start: parseDate('2021-11-22') + }); + const currentPositions = await portfolioCalculator.getCurrentPositions( parseDate('2021-11-22') ); const investments = portfolioCalculator.getInvestments(); - const investmentsByMonth = - portfolioCalculator.getInvestmentsByGroup('month'); + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: chartData, + groupBy: 'month' + }); spy.mockRestore(); @@ -135,7 +141,8 @@ describe('PortfolioCalculator', () => { ]); expect(investmentsByMonth).toEqual([ - { date: '2021-11-01', investment: new Big('12.6') } + { date: '2021-11-01', investment: 0 }, + { date: '2021-12-01', investment: 0 } ]); }); }); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts index 8d416ddc8..6a28cf065 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-baln-buy.spec.ts @@ -57,14 +57,20 @@ describe('PortfolioCalculator', () => { .spyOn(Date, 'now') .mockImplementation(() => parseDate('2021-12-18').getTime()); + const chartData = await portfolioCalculator.getChartData({ + start: parseDate('2021-11-30') + }); + const currentPositions = await portfolioCalculator.getCurrentPositions( parseDate('2021-11-30') ); const investments = portfolioCalculator.getInvestments(); - const investmentsByMonth = - portfolioCalculator.getInvestmentsByGroup('month'); + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: chartData, + groupBy: 'month' + }); spy.mockRestore(); @@ -123,7 +129,8 @@ describe('PortfolioCalculator', () => { ]); expect(investmentsByMonth).toEqual([ - { date: '2021-11-01', investment: new Big('273.2') } + { date: '2021-11-01', investment: 273.2 }, + { date: '2021-12-01', investment: 0 } ]); }); }); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index cb96751f1..767b3e809 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -81,14 +81,20 @@ describe('PortfolioCalculator', () => { .spyOn(Date, 'now') .mockImplementation(() => parseDate('2018-01-01').getTime()); + const chartData = await portfolioCalculator.getChartData({ + start: parseDate('2015-01-01') + }); + const currentPositions = await portfolioCalculator.getCurrentPositions( parseDate('2015-01-01') ); const investments = portfolioCalculator.getInvestments(); - const investmentsByMonth = - portfolioCalculator.getInvestmentsByGroup('month'); + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: chartData, + groupBy: 'month' + }); spy.mockRestore(); @@ -155,42 +161,43 @@ describe('PortfolioCalculator', () => { ]); expect(investmentsByMonth).toEqual([ - { date: '2015-01-01', investment: new Big('640.86') }, - { date: '2015-02-01', investment: new Big('0') }, - { date: '2015-03-01', investment: new Big('0') }, - { date: '2015-04-01', investment: new Big('0') }, - { date: '2015-05-01', investment: new Big('0') }, - { date: '2015-06-01', investment: new Big('0') }, - { date: '2015-07-01', investment: new Big('0') }, - { date: '2015-08-01', investment: new Big('0') }, - { date: '2015-09-01', investment: new Big('0') }, - { date: '2015-10-01', investment: new Big('0') }, - { date: '2015-11-01', investment: new Big('0') }, - { date: '2015-12-01', investment: new Big('0') }, - { date: '2016-01-01', investment: new Big('0') }, - { date: '2016-02-01', investment: new Big('0') }, - { date: '2016-03-01', investment: new Big('0') }, - { date: '2016-04-01', investment: new Big('0') }, - { date: '2016-05-01', investment: new Big('0') }, - { date: '2016-06-01', investment: new Big('0') }, - { date: '2016-07-01', investment: new Big('0') }, - { date: '2016-08-01', investment: new Big('0') }, - { date: '2016-09-01', investment: new Big('0') }, - { date: '2016-10-01', investment: new Big('0') }, - { date: '2016-11-01', investment: new Big('0') }, - { date: '2016-12-01', investment: new Big('0') }, - { date: '2017-01-01', investment: new Big('0') }, - { date: '2017-02-01', investment: new Big('0') }, - { date: '2017-03-01', investment: new Big('0') }, - { date: '2017-04-01', investment: new Big('0') }, - { date: '2017-05-01', investment: new Big('0') }, - { date: '2017-06-01', investment: new Big('0') }, - { date: '2017-07-01', investment: new Big('0') }, - { date: '2017-08-01', investment: new Big('0') }, - { date: '2017-09-01', investment: new Big('0') }, - { date: '2017-10-01', investment: new Big('0') }, - { date: '2017-11-01', investment: new Big('0') }, - { date: '2017-12-01', investment: new Big('-14156.4') } + { date: '2015-01-01', investment: 637.0853345999999 }, + { date: '2015-02-01', investment: 0 }, + { date: '2015-03-01', investment: 0 }, + { date: '2015-04-01', investment: 0 }, + { date: '2015-05-01', investment: 0 }, + { date: '2015-06-01', investment: 0 }, + { date: '2015-07-01', investment: 0 }, + { date: '2015-08-01', investment: 0 }, + { date: '2015-09-01', investment: 0 }, + { date: '2015-10-01', investment: 0 }, + { date: '2015-11-01', investment: 0 }, + { date: '2015-12-01', investment: 0 }, + { date: '2016-01-01', investment: 0 }, + { date: '2016-02-01', investment: 0 }, + { date: '2016-03-01', investment: 0 }, + { date: '2016-04-01', investment: 0 }, + { date: '2016-05-01', investment: 0 }, + { date: '2016-06-01', investment: 0 }, + { date: '2016-07-01', investment: 0 }, + { date: '2016-08-01', investment: 0 }, + { date: '2016-09-01', investment: 0 }, + { date: '2016-10-01', investment: 0 }, + { date: '2016-11-01', investment: 0 }, + { date: '2016-12-01', investment: 0 }, + { date: '2017-01-01', investment: 0 }, + { date: '2017-02-01', investment: 0 }, + { date: '2017-03-01', investment: 0 }, + { date: '2017-04-01', investment: 0 }, + { date: '2017-05-01', investment: 0 }, + { date: '2017-06-01', investment: 0 }, + { date: '2017-07-01', investment: 0 }, + { date: '2017-08-01', investment: 0 }, + { date: '2017-09-01', investment: 0 }, + { date: '2017-10-01', investment: 0 }, + { date: '2017-11-01', investment: 0 }, + { date: '2017-12-01', investment: -318.54266729999995 }, + { date: '2018-01-01', investment: 0 } ]); }); }); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts index fcd3e8bd5..54774dbcf 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-googl-buy.spec.ts @@ -70,14 +70,20 @@ describe('PortfolioCalculator', () => { .spyOn(Date, 'now') .mockImplementation(() => parseDate('2023-07-10').getTime()); + const chartData = await portfolioCalculator.getChartData({ + start: parseDate('2023-01-03') + }); + const currentPositions = await portfolioCalculator.getCurrentPositions( parseDate('2023-01-03') ); const investments = portfolioCalculator.getInvestments(); - const investmentsByMonth = - portfolioCalculator.getInvestmentsByGroup('month'); + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: chartData, + groupBy: 'month' + }); spy.mockRestore(); @@ -137,7 +143,31 @@ describe('PortfolioCalculator', () => { ]); expect(investmentsByMonth).toEqual([ - { date: '2023-01-01', investment: new Big('89.12') } + { date: '2023-01-01', investment: 82.329056 }, + { + date: '2023-02-01', + investment: 0 + }, + { + date: '2023-03-01', + investment: 0 + }, + { + date: '2023-04-01', + investment: 0 + }, + { + date: '2023-05-01', + investment: 0 + }, + { + date: '2023-06-01', + investment: 0 + }, + { + date: '2023-07-01', + investment: 0 + } ]); }); }); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts index aeffaae26..8e3add9ff 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-no-orders.spec.ts @@ -45,14 +45,20 @@ describe('PortfolioCalculator', () => { .spyOn(Date, 'now') .mockImplementation(() => parseDate('2021-12-18').getTime()); + const chartData = await portfolioCalculator.getChartData({ + start: new Date() + }); + const currentPositions = await portfolioCalculator.getCurrentPositions( new Date() ); const investments = portfolioCalculator.getInvestments(); - const investmentsByMonth = - portfolioCalculator.getInvestmentsByGroup('month'); + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: chartData, + groupBy: 'month' + }); spy.mockRestore(); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index 7bf0bc1e1..f891122e4 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -68,14 +68,20 @@ describe('PortfolioCalculator', () => { .spyOn(Date, 'now') .mockImplementation(() => parseDate('2022-04-11').getTime()); + const chartData = await portfolioCalculator.getChartData({ + start: parseDate('2022-03-07') + }); + const currentPositions = await portfolioCalculator.getCurrentPositions( parseDate('2022-03-07') ); const investments = portfolioCalculator.getInvestments(); - const investmentsByMonth = - portfolioCalculator.getInvestmentsByGroup('month'); + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: chartData, + groupBy: 'month' + }); spy.mockRestore(); @@ -137,8 +143,8 @@ describe('PortfolioCalculator', () => { ]); expect(investmentsByMonth).toEqual([ - { date: '2022-03-01', investment: new Big('151.6') }, - { date: '2022-04-01', investment: new Big('-85.73') } + { date: '2022-03-01', investment: 151.6 }, + { date: '2022-04-01', investment: -75.8 } ]); }); }); diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts index 3e30374c4..1a13ba7e5 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -68,9 +68,9 @@ describe('PortfolioCalculator', () => { .spyOn(Date, 'now') .mockImplementation(() => parseDate('2022-04-11').getTime()); - const chartData = await portfolioCalculator.getChartData( - parseDate('2022-03-07') - ); + const chartData = await portfolioCalculator.getChartData({ + start: parseDate('2022-03-07') + }); const currentPositions = await portfolioCalculator.getCurrentPositions( parseDate('2022-03-07') @@ -78,13 +78,16 @@ describe('PortfolioCalculator', () => { const investments = portfolioCalculator.getInvestments(); - const investmentsByMonth = - portfolioCalculator.getInvestmentsByGroup('month'); + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: chartData, + groupBy: 'month' + }); spy.mockRestore(); expect(chartData[0]).toEqual({ date: '2022-03-07', + investmentValueWithCurrencyEffect: 151.6, netPerformance: 0, netPerformanceInPercentage: 0, netPerformanceInPercentageWithCurrencyEffect: 0, @@ -97,6 +100,7 @@ describe('PortfolioCalculator', () => { expect(chartData[chartData.length - 1]).toEqual({ date: '2022-04-11', + investmentValueWithCurrencyEffect: 0, netPerformance: 19.86, netPerformanceInPercentage: 13.100263852242744, netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744, @@ -163,8 +167,8 @@ describe('PortfolioCalculator', () => { ]); expect(investmentsByMonth).toEqual([ - { date: '2022-03-01', investment: new Big('151.6') }, - { date: '2022-04-01', investment: new Big('-171.46') } + { date: '2022-03-01', investment: 151.6 }, + { date: '2022-04-01', investment: -151.6 } ]); }); }); diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 93474ffa5..0813a041e 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -3,6 +3,8 @@ import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfac import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DataProviderInfo, + HistoricalDataItem, + InvestmentItem, ResponseError, SymbolMetrics, TimelinePosition @@ -14,16 +16,11 @@ import Big from 'big.js'; import { addDays, addMilliseconds, - addMonths, - addYears, differenceInDays, endOfDay, format, isBefore, isSameDay, - isSameMonth, - isSameYear, - set, subDays } from 'date-fns'; import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash'; @@ -32,10 +29,6 @@ import { CurrentRateService } from './current-rate.service'; import { CurrentPositions } from './interfaces/current-positions.interface'; import { PortfolioOrderItem } from './interfaces/portfolio-calculator.interface'; import { PortfolioOrder } from './interfaces/portfolio-order.interface'; -import { - Accuracy, - TimelineSpecification -} from './interfaces/timeline-specification.interface'; import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface'; import { TransactionPoint } from './interfaces/transaction-point.interface'; @@ -179,7 +172,15 @@ export class PortfolioCalculator { this.transactionPoints = transactionPoints; } - public async getChartData(start: Date, end = new Date(Date.now()), step = 1) { + public async getChartData({ + end = new Date(Date.now()), + start, + step = 1 + }: { + end?: Date; + start: Date; + step?: number; + }): Promise { const symbols: { [symbol: string]: boolean } = {}; const transactionPointsBeforeEndDate = @@ -203,13 +204,15 @@ export class PortfolioCalculator { dates.push(resetHours(end)); } - for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) { - dataGatheringItems.push({ - dataSource: item.dataSource, - symbol: item.symbol - }); - currencies[item.symbol] = item.currency; - symbols[item.symbol] = true; + if (transactionPointsBeforeEndDate.length > 0) { + for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) { + dataGatheringItems.push({ + dataSource: item.dataSource, + symbol: item.symbol + }); + currencies[item.symbol] = item.currency; + symbols[item.symbol] = true; + } } const { dataProviderInfos, values: marketSymbols } = @@ -248,6 +251,7 @@ export class PortfolioCalculator { const accumulatedValuesByDate: { [date: string]: { + investmentValueWithCurrencyEffect: Big; totalCurrentValue: Big; totalCurrentValueWithCurrencyEffect: Big; totalInvestmentValue: Big; @@ -263,7 +267,8 @@ export class PortfolioCalculator { [symbol: string]: { currentValues: { [date: string]: Big }; currentValuesWithCurrencyEffect: { [date: string]: Big }; - investmentValues: { [date: string]: Big }; + investmentValuesAccumulated: { [date: string]: Big }; + investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big }; investmentValuesWithCurrencyEffect: { [date: string]: Big }; netPerformanceValues: { [date: string]: Big }; netPerformanceValuesWithCurrencyEffect: { [date: string]: Big }; @@ -276,7 +281,8 @@ export class PortfolioCalculator { const { currentValues, currentValuesWithCurrencyEffect, - investmentValues, + investmentValuesAccumulated, + investmentValuesAccumulatedWithCurrencyEffect, investmentValuesWithCurrencyEffect, netPerformanceValues, netPerformanceValuesWithCurrencyEffect, @@ -296,7 +302,8 @@ export class PortfolioCalculator { valuesBySymbol[symbol] = { currentValues, currentValuesWithCurrencyEffect, - investmentValues, + investmentValuesAccumulated, + investmentValuesAccumulatedWithCurrencyEffect, investmentValuesWithCurrencyEffect, netPerformanceValues, netPerformanceValuesWithCurrencyEffect, @@ -318,8 +325,13 @@ export class PortfolioCalculator { symbolValues.currentValuesWithCurrencyEffect?.[dateString] ?? new Big(0); - const investmentValue = - symbolValues.investmentValues?.[dateString] ?? new Big(0); + const investmentValueAccumulated = + symbolValues.investmentValuesAccumulated?.[dateString] ?? new Big(0); + + const investmentValueAccumulatedWithCurrencyEffect = + symbolValues.investmentValuesAccumulatedWithCurrencyEffect?.[ + dateString + ] ?? new Big(0); const investmentValueWithCurrencyEffect = symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ?? @@ -341,6 +353,10 @@ export class PortfolioCalculator { ] ?? new Big(0); accumulatedValuesByDate[dateString] = { + investmentValueWithCurrencyEffect: ( + accumulatedValuesByDate[dateString] + ?.investmentValueWithCurrencyEffect ?? new Big(0) + ).add(investmentValueWithCurrencyEffect), totalCurrentValue: ( accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0) ).add(currentValue), @@ -351,11 +367,11 @@ export class PortfolioCalculator { totalInvestmentValue: ( accumulatedValuesByDate[dateString]?.totalInvestmentValue ?? new Big(0) - ).add(investmentValue), + ).add(investmentValueAccumulated), totalInvestmentValueWithCurrencyEffect: ( accumulatedValuesByDate[dateString] ?.totalInvestmentValueWithCurrencyEffect ?? new Big(0) - ).add(investmentValueWithCurrencyEffect), + ).add(investmentValueAccumulatedWithCurrencyEffect), totalNetPerformanceValue: ( accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ?? new Big(0) @@ -378,6 +394,7 @@ export class PortfolioCalculator { return Object.entries(accumulatedValuesByDate).map(([date, values]) => { const { + investmentValueWithCurrencyEffect, totalCurrentValue, totalCurrentValueWithCurrencyEffect, totalInvestmentValue, @@ -407,6 +424,8 @@ export class PortfolioCalculator { date, netPerformanceInPercentage, netPerformanceInPercentageWithCurrencyEffect, + investmentValueWithCurrencyEffect: + investmentValueWithCurrencyEffect.toNumber(), netPerformance: totalNetPerformanceValue.toNumber(), netPerformanceWithCurrencyEffect: totalNetPerformanceValueWithCurrencyEffect.toNumber(), @@ -671,95 +690,27 @@ export class PortfolioCalculator { }); } - public getInvestmentsByGroup( - groupBy: GroupBy - ): { date: string; investment: Big }[] { - if (this.orders.length === 0) { - return []; - } - - const investments: { date: string; investment: Big }[] = []; - let currentDate: Date; - let investmentByGroup = new Big(0); - - for (const [index, order] of this.orders.entries()) { - if ( - isSameYear(parseDate(order.date), currentDate) && - (groupBy === 'year' || isSameMonth(parseDate(order.date), currentDate)) - ) { - // Same group: Add up investments - investmentByGroup = investmentByGroup.plus( - order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type)) - ); - } else { - // New group: Store previous group and reset - if (currentDate) { - investments.push({ - date: format( - set(currentDate, { - date: 1, - month: groupBy === 'year' ? 0 : currentDate.getMonth() - }), - DATE_FORMAT - ), - investment: investmentByGroup - }); - } - - currentDate = parseDate(order.date); - investmentByGroup = order.quantity - .mul(order.unitPrice) - .mul(this.getFactor(order.type)); - } - - if (index === this.orders.length - 1) { - // Store current group (latest order) - investments.push({ - date: format( - set(currentDate, { - date: 1, - month: groupBy === 'year' ? 0 : currentDate.getMonth() - }), - DATE_FORMAT - ), - investment: investmentByGroup - }); - } - } - - // Fill in the missing dates with investment = 0 - const startDate = parseDate(first(this.orders).date); - const endDate = parseDate(last(this.orders).date); - - const allDates: string[] = []; - currentDate = startDate; - - while (currentDate <= endDate) { - allDates.push( - format( - set(currentDate, { - date: 1, - month: groupBy === 'year' ? 0 : currentDate.getMonth() - }), - DATE_FORMAT - ) + public getInvestmentsByGroup({ + data, + groupBy + }: { + data: HistoricalDataItem[]; + groupBy: GroupBy; + }): InvestmentItem[] { + const groupedData: { [dateGroup: string]: Big } = {}; + + for (const { date, investmentValueWithCurrencyEffect } of data) { + const dateGroup = + groupBy === 'month' ? date.substring(0, 7) : date.substring(0, 4); + groupedData[dateGroup] = (groupedData[dateGroup] ?? new Big(0)).plus( + investmentValueWithCurrencyEffect ); - currentDate.setMonth(currentDate.getMonth() + 1); } - for (const date of allDates) { - const existingInvestment = investments.find((investment) => { - return investment.date === date; - }); - - if (!existingInvestment) { - investments.push({ date, investment: new Big(0) }); - } - } - - return sortBy(investments, ({ date }) => { - return date; - }); + return Object.keys(groupedData).map((dateGroup) => ({ + date: groupBy === 'month' ? `${dateGroup}-01` : `${dateGroup}-01-01`, + investment: groupedData[dateGroup].toNumber() + })); } private calculateOverallPerformance(positions: TimelinePosition[]) { @@ -886,17 +837,6 @@ export class PortfolioCalculator { return factor; } - private addToDate(date: Date, accuracy: Accuracy): Date { - switch (accuracy) { - case 'day': - return addDays(date, 1); - case 'month': - return addMonths(date, 1); - case 'year': - return addYears(date, 1); - } - } - private getSymbolMetrics({ end, exchangeRates, @@ -933,7 +873,10 @@ export class PortfolioCalculator { let initialValueWithCurrencyEffect: Big; let investmentAtStartDate: Big; let investmentAtStartDateWithCurrencyEffect: Big; - const investmentValues: { [date: string]: Big } = {}; + const investmentValuesAccumulated: { [date: string]: Big } = {}; + const investmentValuesAccumulatedWithCurrencyEffect: { + [date: string]: Big; + } = {}; const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {}; let lastAveragePrice = new Big(0); let lastAveragePriceWithCurrencyEffect = new Big(0); @@ -975,7 +918,8 @@ export class PortfolioCalculator { hasErrors: false, initialValue: new Big(0), initialValueWithCurrencyEffect: new Big(0), - investmentValues: {}, + investmentValuesAccumulated: {}, + investmentValuesAccumulatedWithCurrencyEffect: {}, investmentValuesWithCurrencyEffect: {}, netPerformance: new Big(0), netPerformancePercentage: new Big(0), @@ -1014,7 +958,8 @@ export class PortfolioCalculator { hasErrors: true, initialValue: new Big(0), initialValueWithCurrencyEffect: new Big(0), - investmentValues: {}, + investmentValuesAccumulated: {}, + investmentValuesAccumulatedWithCurrencyEffect: {}, investmentValuesWithCurrencyEffect: {}, netPerformance: new Big(0), netPerformancePercentage: new Big(0), @@ -1407,11 +1352,15 @@ export class PortfolioCalculator { feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect) ); - investmentValues[order.date] = totalInvestment; + investmentValuesAccumulated[order.date] = totalInvestment; - investmentValuesWithCurrencyEffect[order.date] = + investmentValuesAccumulatedWithCurrencyEffect[order.date] = totalInvestmentWithCurrencyEffect; + investmentValuesWithCurrencyEffect[order.date] = ( + investmentValuesWithCurrencyEffect[order.date] ?? new Big(0) + ).add(transactionInvestmentWithCurrencyEffect); + timeWeightedInvestmentValues[order.date] = totalInvestmentDays > 0 ? sumOfTimeWeightedInvestments.div(totalInvestmentDays) @@ -1569,7 +1518,8 @@ export class PortfolioCalculator { grossPerformancePercentageWithCurrencyEffect, initialValue, initialValueWithCurrencyEffect, - investmentValues, + investmentValuesAccumulated, + investmentValuesAccumulatedWithCurrencyEffect, investmentValuesWithCurrencyEffect, netPerformancePercentage, netPerformancePercentageWithCurrencyEffect, @@ -1591,15 +1541,4 @@ export class PortfolioCalculator { timeWeightedAverageInvestmentBetweenStartAndEndDateWithCurrencyEffect }; } - - private isNextItemActive( - timelineSpecification: TimelineSpecification[], - currentDate: Date, - i: number - ) { - return ( - i + 1 < timelineSpecification.length && - !isBefore(currentDate, parseDate(timelineSpecification[i + 1].start)) - ); - } } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 580068e7a..e00eca175 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -79,7 +79,7 @@ import { subDays, subYears } from 'date-fns'; -import { isEmpty, last, sortBy, uniq, uniqBy } from 'lodash'; +import { isEmpty, last, uniq, uniqBy } from 'lodash'; import { HistoricalDataContainer, @@ -293,77 +293,32 @@ export class PortfolioService { portfolioCalculator.setTransactionPoints(transactionPoints); + const { items } = await this.getChart({ + dateRange, + impersonationId, + portfolioOrders, + transactionPoints, + userId, + userCurrency: this.request.user.Settings.settings.baseCurrency, + withDataDecimation: false + }); + let investments: InvestmentItem[]; if (groupBy) { - investments = portfolioCalculator - .getInvestmentsByGroup(groupBy) - .map((item) => { - return { - date: item.date, - investment: item.investment.toNumber() - }; - }); - - // Add investment of current group - const dateOfCurrentGroup = format( - set(new Date(), { - date: 1, - month: groupBy === 'year' ? 0 : new Date().getMonth() - }), - DATE_FORMAT - ); - const investmentOfCurrentGroup = investments.filter(({ date }) => { - return date === dateOfCurrentGroup; + investments = portfolioCalculator.getInvestmentsByGroup({ + groupBy, + data: items }); - - if (investmentOfCurrentGroup.length <= 0) { - investments.push({ - date: dateOfCurrentGroup, - investment: 0 - }); - } } else { - investments = portfolioCalculator - .getInvestments() - .map(({ date, investment }) => { - return { - date, - investment: investment.toNumber() - }; - }); - - // Add investment of today - const investmentOfToday = investments.filter(({ date }) => { - return date === format(new Date(), DATE_FORMAT); + investments = items.map(({ date, investmentValueWithCurrencyEffect }) => { + return { + date, + investment: investmentValueWithCurrencyEffect + }; }); - - if (investmentOfToday.length <= 0) { - const pastInvestments = investments.filter(({ date }) => { - return isBefore(parseDate(date), new Date()); - }); - const lastInvestment = pastInvestments[pastInvestments.length - 1]; - - investments.push({ - date: format(new Date(), DATE_FORMAT), - investment: lastInvestment?.investment ?? 0 - }); - } } - investments = sortBy(investments, ({ date }) => { - return date; - }); - - const startDate = this.getStartDate( - dateRange, - parseDate(investments[0]?.date) - ); - - investments = investments.filter(({ date }) => { - return !isBefore(parseDate(date), startDate); - }); - let streaks: PortfolioInvestments['streaks']; if (savingsRate) { @@ -1448,7 +1403,8 @@ export class PortfolioService { portfolioOrders, transactionPoints, userCurrency, - userId + userId, + withDataDecimation = true }: { dateRange?: DateRange; impersonationId: string; @@ -1456,6 +1412,7 @@ export class PortfolioService { transactionPoints: TransactionPoint[]; userCurrency: string; userId: string; + withDataDecimation?: boolean; }): Promise { if (transactionPoints.length === 0) { return { @@ -1481,16 +1438,18 @@ export class PortfolioService { const portfolioStart = parseDate(transactionPoints[0].date); const startDate = this.getStartDate(dateRange, portfolioStart); - const daysInMarket = differenceInDays(new Date(), startDate); - const step = Math.round( - daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS) - ); + let step = 1; - const items = await portfolioCalculator.getChartData( - startDate, - endDate, - step - ); + if (withDataDecimation) { + const daysInMarket = differenceInDays(new Date(), startDate); + step = Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)); + } + + const items = await portfolioCalculator.getChartData({ + step, + end: endDate, + start: startDate + }); return { items, diff --git a/libs/common/src/lib/interfaces/historical-data-item.interface.ts b/libs/common/src/lib/interfaces/historical-data-item.interface.ts index 049a87fb9..0b45cf0b7 100644 --- a/libs/common/src/lib/interfaces/historical-data-item.interface.ts +++ b/libs/common/src/lib/interfaces/historical-data-item.interface.ts @@ -2,6 +2,7 @@ export interface HistoricalDataItem { averagePrice?: number; date: string; grossPerformancePercent?: number; + investmentValueWithCurrencyEffect?: number; marketPrice?: number; netPerformance?: number; netPerformanceInPercentage?: number; diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 491870191..7d77826d0 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -19,6 +19,7 @@ import type { FilterGroup } from './filter-group.interface'; import type { Filter } from './filter.interface'; import type { HistoricalDataItem } from './historical-data-item.interface'; import type { InfoItem } from './info-item.interface'; +import type { InvestmentItem } from './investment-item.interface'; import type { LineChartItem } from './line-chart-item.interface'; import type { PortfolioChart } from './portfolio-chart.interface'; import type { PortfolioDetails } from './portfolio-details.interface'; @@ -74,6 +75,7 @@ export { HistoricalDataItem, ImportResponse, InfoItem, + InvestmentItem, LineChartItem, OAuthResponse, PortfolioChart, diff --git a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts index 71cec81b2..e7cbf7460 100644 --- a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts +++ b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts @@ -14,7 +14,10 @@ export interface SymbolMetrics { hasErrors: boolean; initialValue: Big; initialValueWithCurrencyEffect: Big; - investmentValues: { + investmentValuesAccumulated: { + [date: string]: Big; + }; + investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big; }; investmentValuesWithCurrencyEffect: {