diff --git a/apps/api/src/app/core/portfolio-calculator.spec.ts b/apps/api/src/app/core/portfolio-calculator.spec.ts index bc1e28d29..d957ad54c 100644 --- a/apps/api/src/app/core/portfolio-calculator.spec.ts +++ b/apps/api/src/app/core/portfolio-calculator.spec.ts @@ -1,5 +1,11 @@ -import { PortfolioCalculator, PortfolioOrder } from '@ghostfolio/api/app/core/portfolio-calculator'; -import { CurrentRateService, GetValueParams } from '@ghostfolio/api/app/core/current-rate.service'; +import { + PortfolioCalculator, + PortfolioOrder +} from '@ghostfolio/api/app/core/portfolio-calculator'; +import { + CurrentRateService, + GetValueParams +} from '@ghostfolio/api/app/core/current-rate.service'; import { Currency } from '@prisma/client'; import { OrderType } from '@ghostfolio/api/models/order-type'; import Big from 'big.js'; @@ -16,9 +22,11 @@ function dateEqual(date1: Date, date2: Date) { const date1Converted = toYearMonthDay(date1); const date2Converted = toYearMonthDay(date2); - return date1Converted[0] === date2Converted[0]&& + return ( + date1Converted[0] === date2Converted[0] && date1Converted[1] === date2Converted[1] && date1Converted[2] === date2Converted[2] + ); } jest.mock('./current-rate.service.ts', () => { @@ -26,13 +34,17 @@ jest.mock('./current-rate.service.ts', () => { // eslint-disable-next-line @typescript-eslint/naming-convention CurrentRateService: jest.fn().mockImplementation(() => { return { - getValue: ({date, symbol, currency, userCurrency}: GetValueParams) => { + getValue: ({ + date, + symbol, + currency, + userCurrency + }: GetValueParams) => { const today = new Date(); if (dateEqual(today, date) && symbol === 'VTI') { return Promise.resolve(new Big('213.32')); } - return Promise.resolve(new Big('0')); } }; @@ -41,7 +53,6 @@ jest.mock('./current-rate.service.ts', () => { }); describe('PortfolioCalculator', () => { - let currentRateService: CurrentRateService; beforeEach(() => { currentRateService = new CurrentRateService(null, null); @@ -49,11 +60,17 @@ describe('PortfolioCalculator', () => { describe('calculate transaction points', () => { it('with orders of only one symbol', () => { - const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD); + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); portfolioCalculator.computeTransactionPoints(ordersVTI); - const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints(); + const portfolioItemsAtTransactionPoints = + portfolioCalculator.getTransactionPoints(); - expect(portfolioItemsAtTransactionPoints).toEqual(ordersVTITransactionPoints); + expect(portfolioItemsAtTransactionPoints).toEqual( + ordersVTITransactionPoints + ); }); it('with two orders at the same day of the same type', () => { @@ -68,65 +85,79 @@ describe('PortfolioCalculator', () => { currency: Currency.USD } ]; - const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD); + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); portfolioCalculator.computeTransactionPoints(orders); - const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints(); + const portfolioItemsAtTransactionPoints = + portfolioCalculator.getTransactionPoints(); expect(portfolioItemsAtTransactionPoints).toEqual([ { date: '2019-02-01', - items: [{ - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 1 - }] + items: [ + { + quantity: new Big('10'), + symbol: 'VTI', + investment: new Big('1443.8'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 1 + } + ] }, { date: '2019-08-03', - items: [{ - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 2 - }] + items: [ + { + quantity: new Big('20'), + symbol: 'VTI', + investment: new Big('2923.7'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2 + } + ] }, { date: '2020-02-02', - items: [{ - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('652.55'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 3 - }] + items: [ + { + quantity: new Big('5'), + symbol: 'VTI', + investment: new Big('652.55'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 3 + } + ] }, { date: '2021-02-01', - items: [{ - quantity: new Big('35'), - symbol: 'VTI', - investment: new Big('6627.05'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 5 - }] + items: [ + { + quantity: new Big('35'), + symbol: 'VTI', + investment: new Big('6627.05'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 5 + } + ] }, { date: '2021-08-01', - items: [{ - quantity: new Big('45'), - symbol: 'VTI', - investment: new Big('8403.95'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 6 - }] + items: [ + { + quantity: new Big('45'), + symbol: 'VTI', + investment: new Big('8403.95'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 6 + } + ] } ]); }); @@ -143,104 +174,124 @@ describe('PortfolioCalculator', () => { currency: Currency.USD } ]; - const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD); + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); portfolioCalculator.computeTransactionPoints(orders); - const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints(); + const portfolioItemsAtTransactionPoints = + portfolioCalculator.getTransactionPoints(); expect(portfolioItemsAtTransactionPoints).toEqual([ { date: '2019-02-01', - items: [{ - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 1 - }] + items: [ + { + quantity: new Big('10'), + symbol: 'VTI', + investment: new Big('1443.8'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 1 + } + ] }, { date: '2019-08-03', - items: [{ - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 2 - }] + items: [ + { + quantity: new Big('20'), + symbol: 'VTI', + investment: new Big('2923.7'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2 + } + ] }, { date: '2019-09-01', - items: [{ - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: Currency.USD, - firstBuyDate: '2019-09-01', - transactionCount: 1 - }, { - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 2 - }] + items: [ + { + quantity: new Big('5'), + symbol: 'AMZN', + investment: new Big('10109.95'), + currency: Currency.USD, + firstBuyDate: '2019-09-01', + transactionCount: 1 + }, + { + quantity: new Big('20'), + symbol: 'VTI', + investment: new Big('2923.7'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2 + } + ] }, { date: '2020-02-02', - items: [{ - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: Currency.USD, - firstBuyDate: '2019-09-01', - transactionCount: 1 - }, { - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('652.55'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 3 - }] + items: [ + { + quantity: new Big('5'), + symbol: 'AMZN', + investment: new Big('10109.95'), + currency: Currency.USD, + firstBuyDate: '2019-09-01', + transactionCount: 1 + }, + { + quantity: new Big('5'), + symbol: 'VTI', + investment: new Big('652.55'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 3 + } + ] }, { date: '2021-02-01', - items: [{ - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: Currency.USD, - firstBuyDate: '2019-09-01', - transactionCount: 1 - }, { - quantity: new Big('15'), - symbol: 'VTI', - investment: new Big('2684.05'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 4 - }] + items: [ + { + quantity: new Big('5'), + symbol: 'AMZN', + investment: new Big('10109.95'), + currency: Currency.USD, + firstBuyDate: '2019-09-01', + transactionCount: 1 + }, + { + quantity: new Big('15'), + symbol: 'VTI', + investment: new Big('2684.05'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 4 + } + ] }, { date: '2021-08-01', - items: [{ - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: Currency.USD, - firstBuyDate: '2019-09-01', - transactionCount: 1 - }, { - quantity: new Big('25'), - symbol: 'VTI', - investment: new Big('4460.95'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 5 - }] + items: [ + { + quantity: new Big('5'), + symbol: 'AMZN', + investment: new Big('10109.95'), + currency: Currency.USD, + firstBuyDate: '2019-09-01', + transactionCount: 1 + }, + { + quantity: new Big('25'), + symbol: 'VTI', + investment: new Big('4460.95'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 5 + } + ] } ]); }); @@ -265,173 +316,208 @@ describe('PortfolioCalculator', () => { currency: Currency.USD } ]; - const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD); + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); portfolioCalculator.computeTransactionPoints(orders); - const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints(); + const portfolioItemsAtTransactionPoints = + portfolioCalculator.getTransactionPoints(); expect(portfolioItemsAtTransactionPoints).toEqual([ { date: '2019-02-01', - items: [{ - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 1 - }] + items: [ + { + quantity: new Big('10'), + symbol: 'VTI', + investment: new Big('1443.8'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 1 + } + ] }, { date: '2019-08-03', - items: [{ - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 2 - }] + items: [ + { + quantity: new Big('20'), + symbol: 'VTI', + investment: new Big('2923.7'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2 + } + ] }, { date: '2019-09-01', - items: [{ - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: Currency.USD, - firstBuyDate: '2019-09-01', - transactionCount: 1 - }, { - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 2 - }] + items: [ + { + quantity: new Big('5'), + symbol: 'AMZN', + investment: new Big('10109.95'), + currency: Currency.USD, + firstBuyDate: '2019-09-01', + transactionCount: 1 + }, + { + quantity: new Big('20'), + symbol: 'VTI', + investment: new Big('2923.7'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2 + } + ] }, { date: '2020-02-02', - items: [{ - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: Currency.USD, - firstBuyDate: '2019-09-01', - transactionCount: 1 - }, { - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('652.55'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 3 - }] + items: [ + { + quantity: new Big('5'), + symbol: 'AMZN', + investment: new Big('10109.95'), + currency: Currency.USD, + firstBuyDate: '2019-09-01', + transactionCount: 1 + }, + { + quantity: new Big('5'), + symbol: 'VTI', + investment: new Big('652.55'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 3 + } + ] }, { date: '2020-08-02', - items: [{ - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('652.55'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 3 - }] + items: [ + { + quantity: new Big('5'), + symbol: 'VTI', + investment: new Big('652.55'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 3 + } + ] }, { date: '2021-02-01', - items: [{ - quantity: new Big('15'), - symbol: 'VTI', - investment: new Big('2684.05'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 4 - }] + items: [ + { + quantity: new Big('15'), + symbol: 'VTI', + investment: new Big('2684.05'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 4 + } + ] }, { date: '2021-08-01', - items: [{ - quantity: new Big('25'), - symbol: 'VTI', - investment: new Big('4460.95'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 5 - }] + items: [ + { + quantity: new Big('25'), + symbol: 'VTI', + investment: new Big('4460.95'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 5 + } + ] } ]); }); it('with mixed symbols', () => { - const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD); + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); portfolioCalculator.computeTransactionPoints(ordersMixedSymbols); - const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints(); + const portfolioItemsAtTransactionPoints = + portfolioCalculator.getTransactionPoints(); expect(portfolioItemsAtTransactionPoints).toEqual([ { date: '2017-01-03', - items: [{ - quantity: new Big('50'), - symbol: 'TSLA', - investment: new Big('2148.5'), - currency: Currency.USD, - firstBuyDate: '2017-01-03', - transactionCount: 1 - }] + items: [ + { + quantity: new Big('50'), + symbol: 'TSLA', + investment: new Big('2148.5'), + currency: Currency.USD, + firstBuyDate: '2017-01-03', + transactionCount: 1 + } + ] }, { date: '2017-07-01', - items: [{ - quantity: new Big('0.5614682'), - symbol: 'BTCUSD', - investment: new Big('1999.9999999999998659756'), - currency: Currency.USD, - firstBuyDate: '2017-07-01', - transactionCount: 1 - }, { - quantity: new Big('50'), - symbol: 'TSLA', - investment: new Big('2148.5'), - currency: Currency.USD, - firstBuyDate: '2017-01-03', - transactionCount: 1 - }] + items: [ + { + quantity: new Big('0.5614682'), + symbol: 'BTCUSD', + investment: new Big('1999.9999999999998659756'), + currency: Currency.USD, + firstBuyDate: '2017-07-01', + transactionCount: 1 + }, + { + quantity: new Big('50'), + symbol: 'TSLA', + investment: new Big('2148.5'), + currency: Currency.USD, + firstBuyDate: '2017-01-03', + transactionCount: 1 + } + ] }, { date: '2018-09-01', - items: [{ - quantity: new Big('5'), - symbol: 'AMZN', - investment: new Big('10109.95'), - currency: Currency.USD, - firstBuyDate: '2018-09-01', - transactionCount: 1 - }, { - quantity: new Big('0.5614682'), - symbol: 'BTCUSD', - investment: new Big('1999.9999999999998659756'), - currency: Currency.USD, - firstBuyDate: '2017-07-01', - transactionCount: 1 - }, { - quantity: new Big('50'), - symbol: 'TSLA', - investment: new Big('2148.5'), - currency: Currency.USD, - firstBuyDate: '2017-01-03', - transactionCount: 1 - }] + items: [ + { + quantity: new Big('5'), + symbol: 'AMZN', + investment: new Big('10109.95'), + currency: Currency.USD, + firstBuyDate: '2018-09-01', + transactionCount: 1 + }, + { + quantity: new Big('0.5614682'), + symbol: 'BTCUSD', + investment: new Big('1999.9999999999998659756'), + currency: Currency.USD, + firstBuyDate: '2017-07-01', + transactionCount: 1 + }, + { + quantity: new Big('50'), + symbol: 'TSLA', + investment: new Big('2148.5'), + currency: Currency.USD, + firstBuyDate: '2017-01-03', + transactionCount: 1 + } + ] } ]); }); }); describe('get current positions', () => { - it('with just VTI', async () => { - const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD); + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints); const currentPositions = await portfolioCalculator.getCurrentPositions(); @@ -447,12 +533,8 @@ describe('PortfolioCalculator', () => { transactionCount: 5 } }); - - - }) - - }) - + }); + }); }); const ordersMixedSymbols: PortfolioOrder[] = [ { @@ -527,57 +609,67 @@ const ordersVTI: PortfolioOrder[] = [ const ordersVTITransactionPoints = [ { date: '2019-02-01', - items: [{ - quantity: new Big('10'), - symbol: 'VTI', - investment: new Big('1443.8'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 1 - }] + items: [ + { + quantity: new Big('10'), + symbol: 'VTI', + investment: new Big('1443.8'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 1 + } + ] }, { date: '2019-08-03', - items: [{ - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 2 - }] + items: [ + { + quantity: new Big('20'), + symbol: 'VTI', + investment: new Big('2923.7'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2 + } + ] }, { date: '2020-02-02', - items: [{ - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('652.55'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 3 - }] + items: [ + { + quantity: new Big('5'), + symbol: 'VTI', + investment: new Big('652.55'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 3 + } + ] }, { date: '2021-02-01', - items: [{ - quantity: new Big('15'), - symbol: 'VTI', - investment: new Big('2684.05'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 4 - }] + items: [ + { + quantity: new Big('15'), + symbol: 'VTI', + investment: new Big('2684.05'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 4 + } + ] }, { date: '2021-08-01', - items: [{ - quantity: new Big('25'), - symbol: 'VTI', - investment: new Big('4460.95'), - currency: Currency.USD, - firstBuyDate: '2019-02-01', - transactionCount: 5 - }] + items: [ + { + quantity: new Big('25'), + symbol: 'VTI', + investment: new Big('4460.95'), + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 5 + } + ] } ]; diff --git a/apps/api/src/app/core/portfolio-calculator.ts b/apps/api/src/app/core/portfolio-calculator.ts index 751d9a888..c166a39d5 100644 --- a/apps/api/src/app/core/portfolio-calculator.ts +++ b/apps/api/src/app/core/portfolio-calculator.ts @@ -4,22 +4,16 @@ import { OrderType } from '@ghostfolio/api/models/order-type'; import Big from 'big.js'; export class PortfolioCalculator { - private transactionPoints: TransactionPoint[]; constructor( private currentRateService: CurrentRateService, private currency: Currency - ) { - } + ) {} - addOrder(order: PortfolioOrder): void { - - } + addOrder(order: PortfolioOrder): void {} - deleteOrder(order: PortfolioOrder): void { - - } + deleteOrder(order: PortfolioOrder): void {} computeTransactionPoints(orders: PortfolioOrder[]) { orders.sort((a, b) => a.date.localeCompare(b.date)); @@ -39,9 +33,14 @@ export class PortfolioCalculator { const unitPrice = new Big(order.unitPrice); if (oldAccumulatedSymbol) { currentTransactionPointItem = { - quantity: order.quantity.mul(factor).plus(oldAccumulatedSymbol.quantity), + quantity: order.quantity + .mul(factor) + .plus(oldAccumulatedSymbol.quantity), symbol: order.symbol, - investment: unitPrice.mul(order.quantity).mul(factor).add(oldAccumulatedSymbol.investment), + investment: unitPrice + .mul(order.quantity) + .mul(factor) + .add(oldAccumulatedSymbol.investment), currency: order.currency, firstBuyDate: oldAccumulatedSymbol.firstBuyDate, transactionCount: oldAccumulatedSymbol.transactionCount + 1 @@ -60,7 +59,9 @@ export class PortfolioCalculator { symbols[order.symbol] = currentTransactionPointItem; const items = lastTransactionPoint?.items ?? []; - const newItems = items.filter(transactionPointItem => transactionPointItem.symbol !== order.symbol); + const newItems = items.filter( + (transactionPointItem) => transactionPointItem.symbol !== order.symbol + ); if (!currentTransactionPointItem.quantity.eq(0)) { newItems.push(currentTransactionPointItem); } else { @@ -93,7 +94,8 @@ export class PortfolioCalculator { return {}; } - const lastTransactionPoint = this.transactionPoints[this.transactionPoints.length - 1]; + const lastTransactionPoint = + this.transactionPoints[this.transactionPoints.length - 1]; const result: { [symbol: string]: TimelinePosition } = {}; for (const item of lastTransactionPoint.items) { @@ -117,7 +119,10 @@ export class PortfolioCalculator { return result; } - calculateTimeline(timelineSpecification: TimelineSpecification[], endDate: Date): TimelinePeriod[] { + calculateTimeline( + timelineSpecification: TimelineSpecification[], + endDate: Date + ): TimelinePeriod[] { return null; } @@ -136,7 +141,6 @@ export class PortfolioCalculator { } return factor; } - } interface TransactionPoint { @@ -167,7 +171,7 @@ type Accuracy = 'year' | 'month' | 'day'; interface TimelineSpecification { start: Date; - accuracy: Accuracy + accuracy: Accuracy; } interface TimelinePeriod {