From 693d6535e88e100a8f18961a2d8b4c599aac2465 Mon Sep 17 00:00:00 2001 From: Daniel Devaud Date: Fri, 23 Feb 2024 17:40:40 +0100 Subject: [PATCH] Fix tests --- ...-calculator-novn-baln-buy-and-sell.spec.ts | 152 ++++++++++++++++++ ...folio-calculator-novn-buy-and-sell.spec.ts | 2 + .../src/app/portfolio/portfolio-calculator.ts | 47 +++--- 3 files changed, 182 insertions(+), 19 deletions(-) create mode 100644 apps/api/src/app/portfolio/portfolio-calculator-novn-baln-buy-and-sell.spec.ts diff --git a/apps/api/src/app/portfolio/portfolio-calculator-novn-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator-novn-baln-buy-and-sell.spec.ts new file mode 100644 index 000000000..46da916c5 --- /dev/null +++ b/apps/api/src/app/portfolio/portfolio-calculator-novn-baln-buy-and-sell.spec.ts @@ -0,0 +1,152 @@ +import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.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; + let exchangeRateDataService: ExchangeRateDataService; + + beforeEach(() => { + currentRateService = new CurrentRateService(null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + }); + + describe('get current positions', () => { + it.only('with NOVN.SW and BALN.SW buy and sell', async () => { + const portfolioCalculator = new PortfolioCalculator({ + currentRateService, + exchangeRateDataService, + 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-01', + dataSource: 'YAHOO', + fee: new Big(0), + name: 'Novartis AG', + quantity: new Big(0), + symbol: 'NOVN.SW', + type: 'BUY', + unitPrice: new Big(80.0) + }, + { + 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) + }, + { + currency: 'CHF', + date: '2022-03-22', + dataSource: 'YAHOO', + fee: new Big(1.55), + name: 'Bâloise Holding AG', + quantity: new Big(2), + symbol: 'BALN.SW', + type: 'BUY', + unitPrice: new Big(142.9) + }, + { + currency: 'CHF', + date: '2022-04-01', + dataSource: 'YAHOO', + fee: new Big(0), + name: 'Bâloise Holding AG', + quantity: new Big(0), + symbol: 'BALN.SW', + type: 'BUY', + unitPrice: new Big(138) + }, + { + currency: 'CHF', + date: '2022-04-10', + dataSource: 'YAHOO', + fee: new Big(1.65), + name: 'Bâloise Holding AG', + quantity: new Big(2), + symbol: 'BALN.SW', + type: 'SELL', + unitPrice: new Big(136.6) + } + ] + }); + + portfolioCalculator.computeTransactionPoints(); + + const spy = jest + .spyOn(Date, 'now') + .mockImplementation(() => parseDate('2022-04-11').getTime()); + + const chartData = await portfolioCalculator.getChartData({ + start: parseDate('2022-03-07'), + calculateTimeWeightedPerformance: true + }); + + spy.mockRestore(); + + expect(chartData[0]).toEqual({ + date: '2022-03-07', + investmentValueWithCurrencyEffect: 151.6, + netPerformance: 0, + netPerformanceInPercentage: 0, + netPerformanceInPercentageWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0, + timeWeightedPerformance: 0, + totalInvestment: 151.6, + totalInvestmentValueWithCurrencyEffect: 151.6, + value: 151.6, + valueWithCurrencyEffect: 151.6 + }); + + expect(chartData[chartData.length - 1]).toEqual({ + date: '2022-04-11', + investmentValueWithCurrencyEffect: 0, + netPerformance: 19.86, + netPerformanceInPercentage: 13.100263852242744, + netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744, + netPerformanceWithCurrencyEffect: 19.86, + timeWeightedPerformance: 13.100263852242744, + totalInvestment: 0, + totalInvestmentValueWithCurrencyEffect: 0, + value: 0, + valueWithCurrencyEffect: 0 + }); + }); + }); +}); 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 ada9acc16..a3d311481 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 @@ -93,6 +93,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentage: 0, netPerformanceInPercentageWithCurrencyEffect: 0, netPerformanceWithCurrencyEffect: 0, + timeWeightedPerformance: 0, totalInvestment: 151.6, totalInvestmentValueWithCurrencyEffect: 151.6, value: 151.6, @@ -106,6 +107,7 @@ describe('PortfolioCalculator', () => { netPerformanceInPercentage: 13.100263852242744, netPerformanceInPercentageWithCurrencyEffect: 13.100263852242744, netPerformanceWithCurrencyEffect: 19.86, + timeWeightedPerformance: 0, totalInvestment: 0, totalInvestmentValueWithCurrencyEffect: 0, value: 0, diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 3a5fecc30..ce4c3fe8e 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -270,7 +270,12 @@ export class PortfolioCalculator { [date: string]: { [symbol: string]: Big }; } = {}; + if (!marketSymbols?.length) { + return []; + } + this.populateMarketSymbolMap(marketSymbols, marketSymbolMap); + const accumulatedValuesByDate: { [date: string]: { investmentValueWithCurrencyEffect: Big; @@ -282,6 +287,7 @@ export class PortfolioCalculator { totalNetPerformanceValueWithCurrencyEffect: Big; totalTimeWeightedInvestmentValue: Big; totalTimeWeightedInvestmentValueWithCurrencyEffect: Big; + totalTimeWeightedPerformance: Big; }; } = {}; @@ -360,6 +366,7 @@ export class PortfolioCalculator { totalNetPerformanceValueWithCurrencyEffect: Big; totalTimeWeightedInvestmentValue: Big; totalTimeWeightedInvestmentValueWithCurrencyEffect: Big; + totalTimeWeightedPerformance: Big; }; } ) { @@ -368,19 +375,13 @@ export class PortfolioCalculator { ? format(previousDate, DATE_FORMAT) : null; let totalCurrentValue = new Big(0); - let totalInvestmentValue = new Big(0); let maxTotalInvestmentValue = new Big(0); - let totalNetPerformanceValue = new Big(0); let previousTotalInvestmentValue = new Big(0); let timeWeightedPerformance = new Big(0); if (calculateTimeWeightedPerformance && previousDateString) { - for (const symbol of Object.keys(valuesBySymbol)) { - const symbolValues = valuesBySymbol[symbol]; - previousTotalInvestmentValue = previousTotalInvestmentValue.plus( - symbolValues.currentValues?.[previousDateString] ?? new Big(0) - ); - } + previousTotalInvestmentValue = + accumulatedValuesByDate[previousDateString].totalInvestmentValue; } for (const symbol of Object.keys(valuesBySymbol)) { @@ -390,10 +391,6 @@ export class PortfolioCalculator { totalCurrentValue = totalCurrentValue.plus(symbolCurrentValues); - totalNetPerformanceValue = totalNetPerformanceValue.plus( - symbolValues.netPerformanceValues?.[dateString] ?? new Big(0) - ); - if ( previousTotalInvestmentValue.toNumber() && symbolValues.netPerformanceValuesPercentage && @@ -415,11 +412,17 @@ export class PortfolioCalculator { ); } + let totalTimeWeightedPerformance = timeWeightedPerformance.plus( + accumulatedValuesByDate[previousDateString] + ?.totalTimeWeightedPerformance ?? new Big(0) + ); + accumulatedValuesByDate = this.accumulatedValuesByDate( valuesBySymbol, symbol, dateString, - accumulatedValuesByDate + accumulatedValuesByDate, + totalTimeWeightedPerformance ); } @@ -429,13 +432,16 @@ export class PortfolioCalculator { totalInvestmentValueWithCurrencyEffect, totalNetPerformanceValueWithCurrencyEffect, totalTimeWeightedInvestmentValue, - totalTimeWeightedInvestmentValueWithCurrencyEffect + totalTimeWeightedInvestmentValueWithCurrencyEffect, + totalInvestmentValue, + totalTimeWeightedPerformance, + totalNetPerformanceValue } = accumulatedValuesByDate[dateString]; - const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0) + const netPerformanceInPercentage = totalTimeWeightedInvestmentValue.eq(0) ? 0 : totalNetPerformanceValue - .div(maxTotalInvestmentValue) + .div(totalTimeWeightedInvestmentValue) .mul(100) .toNumber(); @@ -455,7 +461,7 @@ export class PortfolioCalculator { totalInvestment: totalInvestmentValue.toNumber(), value: totalCurrentValue.toNumber(), valueWithCurrencyEffect: totalCurrentValueWithCurrencyEffect.toNumber(), - timeWeightedPerformance: timeWeightedPerformance.toNumber(), + timeWeightedPerformance: totalTimeWeightedPerformance.toNumber(), investmentValueWithCurrencyEffect: investmentValueWithCurrencyEffect.toNumber(), netPerformanceWithCurrencyEffect: @@ -493,8 +499,10 @@ export class PortfolioCalculator { totalNetPerformanceValueWithCurrencyEffect: Big; totalTimeWeightedInvestmentValue: Big; totalTimeWeightedInvestmentValueWithCurrencyEffect: Big; + totalTimeWeightedPerformance: Big; }; - } + }, + timeWeightedPerformance: Big ) { const symbolValues = valuesBySymbol[symbol]; @@ -564,7 +572,8 @@ export class PortfolioCalculator { totalTimeWeightedInvestmentValueWithCurrencyEffect: ( accumulatedValuesByDate[dateString] ?.totalTimeWeightedInvestmentValueWithCurrencyEffect ?? new Big(0) - ).add(timeWeightedInvestmentValueWithCurrencyEffect) + ).add(timeWeightedInvestmentValueWithCurrencyEffect), + totalTimeWeightedPerformance: timeWeightedPerformance }; return accumulatedValuesByDate;