diff --git a/.vscode/launch.json b/.vscode/launch.json index fa7c688b8..1be39f937 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,11 +5,11 @@ "name": "Debug Jest File", "type": "node", "request": "launch", - "program": "${workspaceFolder}/node_modules/@angular/cli/bin/ng", + "program": "${workspaceFolder}/node_modules/@nrwl/cli/bin/nx", "args": [ "test", "--codeCoverage=false", - "--testFile=${workspaceFolder}/apps/api/src/models/portfolio.spec.ts" + "--testFile=${workspaceFolder}/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts" ], "cwd": "${workspaceFolder}", "console": "internalConsole" diff --git a/CHANGELOG.md b/CHANGELOG.md index 159c79442..76b597c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Restructured the actions in the admin control panel +### Fixed + +- Fixed the calculation in the portfolio evolution chart + ## 1.207.0 - 31.10.2022 ### Added 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 new file mode 100644 index 000000000..a27eba341 --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -0,0 +1,130 @@ +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { parseDate } from '@ghostfolio/common/helper'; +import Big from 'big.js'; + +import { CurrentRateServiceMock } from './current-rate.service.mock'; +import { PortfolioCalculator } from './portfolio-calculator'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let currentRateService: CurrentRateService; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null, null); + }); + + describe('get current positions', () => { + it.only('with NOVN.SW buy and sell', async () => { + const portfolioCalculator = new PortfolioCalculator({ + currentRateService, + currency: 'CHF', + orders: [ + { + currency: 'CHF', + date: '2022-03-07', + dataSource: 'YAHOO', + fee: new Big(0), + name: 'Novartis AG', + quantity: new Big(2), + symbol: 'NOVN.SW', + type: 'BUY', + unitPrice: new Big(75.8) + }, + { + currency: 'CHF', + date: '2022-04-08', + dataSource: 'YAHOO', + fee: new Big(0), + name: 'Novartis AG', + quantity: new Big(2), + symbol: 'NOVN.SW', + type: 'SELL', + unitPrice: new Big(85.73) + } + ] + }); + + portfolioCalculator.computeTransactionPoints(); + + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2022-04-11').getTime()); + + const chartData = await portfolioCalculator.getChartData( + parseDate('2022-03-07') + ); + + const currentPositions = await portfolioCalculator.getCurrentPositions( + parseDate('2022-03-07') + ); + + const investments = portfolioCalculator.getInvestments(); + + const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth(); + + spy.mockRestore(); + + expect(chartData[0]).toEqual({ + date: '2022-03-07', + netPerformanceInPercentage: 0, + netPerformance: 0, + totalInvestment: 151.6, + value: 151.6 + }); + + expect(chartData[chartData.length - 1]).toEqual({ + date: '2022-04-11', + netPerformanceInPercentage: 13.100263852242744, + netPerformance: 19.86, + totalInvestment: 0, + value: 19.86 + }); + + expect(currentPositions).toEqual({ + currentValue: new Big('0'), + errors: [], + grossPerformance: new Big('19.86'), + grossPerformancePercentage: new Big('0.13100263852242744063'), + hasErrors: false, + netPerformance: new Big('19.86'), + netPerformancePercentage: new Big('0.13100263852242744063'), + positions: [ + { + averagePrice: new Big('0'), + currency: 'CHF', + dataSource: 'YAHOO', + firstBuyDate: '2022-03-07', + grossPerformance: new Big('19.86'), + grossPerformancePercentage: new Big('0.13100263852242744063'), + investment: new Big('0'), + netPerformance: new Big('19.86'), + netPerformancePercentage: new Big('0.13100263852242744063'), + marketPrice: 87.8, + quantity: new Big('0'), + symbol: 'NOVN.SW', + transactionCount: 2 + } + ], + totalInvestment: new Big('0') + }); + + expect(investments).toEqual([ + { date: '2022-03-07', investment: new Big('151.6') }, + { date: '2022-04-08', investment: new Big('0') } + ]); + + expect(investmentsByMonth).toEqual([ + { date: '2022-03-01', investment: new Big('151.6') }, + { date: '2022-04-01', investment: new Big('-171.46') } + ]); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 4a09141ca..953986cf5 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -234,21 +234,28 @@ export class PortfolioCalculator { [symbol: string]: { [date: string]: Big }; } = {}; + const maxInvestmentValuesBySymbol: { + [symbol: string]: { [date: string]: Big }; + } = {}; + const totalNetPerformanceValues: { [date: string]: Big } = {}; const totalInvestmentValues: { [date: string]: Big } = {}; + const maxTotalInvestmentValues: { [date: string]: Big } = {}; for (const symbol of Object.keys(symbols)) { - const { netPerformanceValues, investmentValues } = this.getSymbolMetrics({ - end, - marketSymbolMap, - start, - step, - symbol, - isChartMode: true - }); + const { investmentValues, maxInvestmentValues, netPerformanceValues } = + this.getSymbolMetrics({ + end, + marketSymbolMap, + start, + step, + symbol, + isChartMode: true + }); netPerformanceValuesBySymbol[symbol] = netPerformanceValues; investmentValuesBySymbol[symbol] = investmentValues; + maxInvestmentValuesBySymbol[symbol] = maxInvestmentValues; } for (const currentDate of dates) { @@ -267,19 +274,28 @@ export class PortfolioCalculator { totalInvestmentValues[dateString] = totalInvestmentValues[dateString] ?? new Big(0); + maxTotalInvestmentValues[dateString] = + maxTotalInvestmentValues[dateString] ?? new Big(0); + if (investmentValuesBySymbol[symbol]?.[dateString]) { totalInvestmentValues[dateString] = totalInvestmentValues[ dateString ].add(investmentValuesBySymbol[symbol][dateString]); } + + if (maxInvestmentValuesBySymbol[symbol]?.[dateString]) { + maxTotalInvestmentValues[dateString] = maxTotalInvestmentValues[ + dateString + ].add(maxInvestmentValuesBySymbol[symbol][dateString]); + } } } return Object.keys(totalNetPerformanceValues).map((date) => { - const netPerformanceInPercentage = totalInvestmentValues[date].eq(0) + const netPerformanceInPercentage = maxTotalInvestmentValues[date].eq(0) ? 0 : totalNetPerformanceValues[date] - .div(totalInvestmentValues[date]) + .div(maxTotalInvestmentValues[date]) .mul(100) .toNumber(); @@ -899,6 +915,7 @@ export class PortfolioCalculator { let initialValue: Big; let investmentAtStartDate: Big; const investmentValues: { [date: string]: Big } = {}; + const maxInvestmentValues: { [date: string]: Big } = {}; let lastAveragePrice = new Big(0); // let lastTransactionInvestment = new Big(0); // let lastValueOfInvestmentBeforeTransaction = new Big(0); @@ -1170,7 +1187,8 @@ export class PortfolioCalculator { .minus(grossPerformanceAtStartDate) .minus(fees.minus(feesAtStartDate)); - investmentValues[order.date] = maxTotalInvestment; + investmentValues[order.date] = totalInvestment; + maxInvestmentValues[order.date] = maxTotalInvestment; } if (PortfolioCalculator.ENABLE_LOGGING) { @@ -1255,6 +1273,7 @@ export class PortfolioCalculator { Average price: ${averagePriceAtStartDate.toFixed( 2 )} -> ${averagePriceAtEndDate.toFixed(2)} + Total investment: ${totalInvestment.toFixed(2)} Max. total investment: ${maxTotalInvestment.toFixed(2)} Gross performance: ${totalGrossPerformance.toFixed( 2 @@ -1270,6 +1289,7 @@ export class PortfolioCalculator { initialValue, grossPerformancePercentage, investmentValues, + maxInvestmentValues, netPerformancePercentage, netPerformanceValues, hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), diff --git a/package.json b/package.json index 56696691d..d26444e95 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "start:server": "nx run api:serve --watch", "start:storybook": "nx run ui:storybook", "test": "nx test", - "test:single": "nx test --test-file portfolio-calculator-novn-buy-and-sell-partially.spec.ts", + "test:single": "nx test --test-file portfolio-calculator-novn-buy-and-sell.spec.ts", "ts-node": "ts-node", "update": "nx migrate latest", "watch:server": "nx run api:build --watch", diff --git a/test/import/ok-novn-buy-and-sell.json b/test/import/ok-novn-buy-and-sell.json new file mode 100644 index 000000000..b8a62279d --- /dev/null +++ b/test/import/ok-novn-buy-and-sell.json @@ -0,0 +1,28 @@ +{ + "meta": { + "date": "2022-07-21T21:28:05.857Z", + "version": "dev" + }, + "activities": [ + { + "fee": 0, + "quantity": 2, + "type": "SELL", + "unitPrice": 85.73, + "currency": "CHF", + "dataSource": "YAHOO", + "date": "2022-04-07T22:00:00.000Z", + "symbol": "NOVN.SW" + }, + { + "fee": 0, + "quantity": 2, + "type": "BUY", + "unitPrice": 75.8, + "currency": "CHF", + "dataSource": "YAHOO", + "date": "2022-03-06T23:00:00.000Z", + "symbol": "NOVN.SW" + } + ] +}