From 7d76f5f7b811a7ca5b1313d95aba2cef4c327146 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 Feb 2024 09:06:41 +0100 Subject: [PATCH] Refactoring --- ...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 | 10 +- .../src/app/portfolio/portfolio-calculator.ts | 127 ++++-------------- .../src/app/portfolio/portfolio.service.ts | 28 +--- libs/common/src/lib/interfaces/index.ts | 2 + 10 files changed, 156 insertions(+), 180 deletions(-) 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 6a356e6e3..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 @@ -78,8 +78,10 @@ describe('PortfolioCalculator', () => { const investments = portfolioCalculator.getInvestments(); - const investmentsByMonth = - portfolioCalculator.getInvestmentsByGroup('month'); + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: chartData, + groupBy: 'month' + }); spy.mockRestore(); @@ -165,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 d5585d255..0813a041e 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -4,6 +4,7 @@ import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DataProviderInfo, HistoricalDataItem, + InvestmentItem, ResponseError, SymbolMetrics, TimelinePosition @@ -20,9 +21,6 @@ import { format, isBefore, isSameDay, - isSameMonth, - isSameYear, - set, subDays } from 'date-fns'; import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash'; @@ -206,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 } = @@ -690,98 +690,27 @@ export class PortfolioCalculator { }); } - /** - * @deprecated - */ - 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[]) { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 9e6fedea2..e00eca175 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -306,7 +306,10 @@ export class PortfolioService { let investments: InvestmentItem[]; if (groupBy) { - investments = this.getInvestmentsByGroup({ groupBy, data: items }); + investments = portfolioCalculator.getInvestmentsByGroup({ + groupBy, + data: items + }); } else { investments = items.map(({ date, investmentValueWithCurrencyEffect }) => { return { @@ -1599,29 +1602,6 @@ export class PortfolioService { }; } - private 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 - ); - } - - return Object.keys(groupedData).map((dateGroup) => ({ - date: groupBy === 'month' ? `${dateGroup}-01` : `${dateGroup}-01-01`, - investment: groupedData[dateGroup].toNumber() - })); - } - private getStartDate(aDateRange: DateRange, portfolioStart: Date) { switch (aDateRange) { case '1d': 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,