diff --git a/apps/api/src/app/core/portfolio-calculator.spec.ts b/apps/api/src/app/core/portfolio-calculator.spec.ts index 30b9fd693..bc1e28d29 100644 --- a/apps/api/src/app/core/portfolio-calculator.spec.ts +++ b/apps/api/src/app/core/portfolio-calculator.spec.ts @@ -1,16 +1,39 @@ import { PortfolioCalculator, PortfolioOrder } from '@ghostfolio/api/app/core/portfolio-calculator'; -import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service'; +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'; +function toYearMonthDay(date: Date) { + const year = date.getFullYear(); + const month = date.getMonth() + 1; + const day = date.getDate(); + + return [year, month, day]; +} + +function dateEqual(date1: Date, date2: Date) { + const date1Converted = toYearMonthDay(date1); + const date2Converted = toYearMonthDay(date2); + + return date1Converted[0] === date2Converted[0]&& + date1Converted[1] === date2Converted[1] && + date1Converted[2] === date2Converted[2] +} + jest.mock('./current-rate.service.ts', () => { return { // eslint-disable-next-line @typescript-eslint/naming-convention CurrentRateService: jest.fn().mockImplementation(() => { return { - getValue: (date: Date, symbol: string, currency: Currency) => { - return 4; + 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')); } }; }) @@ -26,56 +49,11 @@ describe('PortfolioCalculator', () => { describe('calculate transaction points', () => { it('with orders of only one symbol', () => { - const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, ordersVTI); - const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints(); + const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD); + portfolioCalculator.computeTransactionPoints(ordersVTI); + 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 - }] - }, - { - date: '2019-08-03', - items: [{ - quantity: new Big('20'), - symbol: 'VTI', - investment: new Big('2923.7'), - currency: Currency.USD - }] - }, - { - date: '2020-02-02', - items: [{ - quantity: new Big('5'), - symbol: 'VTI', - investment: new Big('652.55'), - currency: Currency.USD - }] - }, - { - date: '2021-02-01', - items: [{ - quantity: new Big('15'), - symbol: 'VTI', - investment: new Big('2684.05'), - currency: Currency.USD - }] - }, - { - date: '2021-08-01', - items: [{ - quantity: new Big('25'), - symbol: 'VTI', - investment: new Big('4460.95'), - currency: Currency.USD - }] - } - ]); + expect(portfolioItemsAtTransactionPoints).toEqual(ordersVTITransactionPoints); }); it('with two orders at the same day of the same type', () => { @@ -90,8 +68,9 @@ describe('PortfolioCalculator', () => { currency: Currency.USD } ]; - const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, orders); - const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints(); + const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD); + portfolioCalculator.computeTransactionPoints(orders); + const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints(); expect(portfolioItemsAtTransactionPoints).toEqual([ { @@ -100,7 +79,9 @@ describe('PortfolioCalculator', () => { quantity: new Big('10'), symbol: 'VTI', investment: new Big('1443.8'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 1 }] }, { @@ -109,7 +90,9 @@ describe('PortfolioCalculator', () => { quantity: new Big('20'), symbol: 'VTI', investment: new Big('2923.7'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2 }] }, { @@ -118,7 +101,9 @@ describe('PortfolioCalculator', () => { quantity: new Big('5'), symbol: 'VTI', investment: new Big('652.55'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 3 }] }, { @@ -127,7 +112,9 @@ describe('PortfolioCalculator', () => { quantity: new Big('35'), symbol: 'VTI', investment: new Big('6627.05'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 5 }] }, { @@ -136,7 +123,9 @@ describe('PortfolioCalculator', () => { quantity: new Big('45'), symbol: 'VTI', investment: new Big('8403.95'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 6 }] } ]); @@ -154,8 +143,9 @@ describe('PortfolioCalculator', () => { currency: Currency.USD } ]; - const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, orders); - const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints(); + const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD); + portfolioCalculator.computeTransactionPoints(orders); + const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints(); expect(portfolioItemsAtTransactionPoints).toEqual([ { @@ -164,7 +154,9 @@ describe('PortfolioCalculator', () => { quantity: new Big('10'), symbol: 'VTI', investment: new Big('1443.8'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 1 }] }, { @@ -173,7 +165,9 @@ describe('PortfolioCalculator', () => { quantity: new Big('20'), symbol: 'VTI', investment: new Big('2923.7'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2 }] }, { @@ -182,12 +176,16 @@ describe('PortfolioCalculator', () => { quantity: new Big('5'), symbol: 'AMZN', investment: new Big('10109.95'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-09-01', + transactionCount: 1 }, { quantity: new Big('20'), symbol: 'VTI', investment: new Big('2923.7'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2 }] }, { @@ -196,12 +194,16 @@ describe('PortfolioCalculator', () => { quantity: new Big('5'), symbol: 'AMZN', investment: new Big('10109.95'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-09-01', + transactionCount: 1 }, { quantity: new Big('5'), symbol: 'VTI', investment: new Big('652.55'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 3 }] }, { @@ -210,12 +212,16 @@ describe('PortfolioCalculator', () => { quantity: new Big('5'), symbol: 'AMZN', investment: new Big('10109.95'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-09-01', + transactionCount: 1 }, { quantity: new Big('15'), symbol: 'VTI', investment: new Big('2684.05'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 4 }] }, { @@ -224,12 +230,16 @@ describe('PortfolioCalculator', () => { quantity: new Big('5'), symbol: 'AMZN', investment: new Big('10109.95'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-09-01', + transactionCount: 1 }, { quantity: new Big('25'), symbol: 'VTI', investment: new Big('4460.95'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 5 }] } ]); @@ -255,8 +265,9 @@ describe('PortfolioCalculator', () => { currency: Currency.USD } ]; - const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, orders); - const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints(); + const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD); + portfolioCalculator.computeTransactionPoints(orders); + const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints(); expect(portfolioItemsAtTransactionPoints).toEqual([ { @@ -265,7 +276,9 @@ describe('PortfolioCalculator', () => { quantity: new Big('10'), symbol: 'VTI', investment: new Big('1443.8'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 1 }] }, { @@ -274,7 +287,9 @@ describe('PortfolioCalculator', () => { quantity: new Big('20'), symbol: 'VTI', investment: new Big('2923.7'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2 }] }, { @@ -283,12 +298,16 @@ describe('PortfolioCalculator', () => { quantity: new Big('5'), symbol: 'AMZN', investment: new Big('10109.95'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-09-01', + transactionCount: 1 }, { quantity: new Big('20'), symbol: 'VTI', investment: new Big('2923.7'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 2 }] }, { @@ -297,12 +316,16 @@ describe('PortfolioCalculator', () => { quantity: new Big('5'), symbol: 'AMZN', investment: new Big('10109.95'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-09-01', + transactionCount: 1 }, { quantity: new Big('5'), symbol: 'VTI', investment: new Big('652.55'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 3 }] }, { @@ -311,7 +334,9 @@ describe('PortfolioCalculator', () => { quantity: new Big('5'), symbol: 'VTI', investment: new Big('652.55'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 3 }] }, { @@ -320,7 +345,9 @@ describe('PortfolioCalculator', () => { quantity: new Big('15'), symbol: 'VTI', investment: new Big('2684.05'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 4 }] }, { @@ -329,15 +356,18 @@ describe('PortfolioCalculator', () => { quantity: new Big('25'), symbol: 'VTI', investment: new Big('4460.95'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2019-02-01', + transactionCount: 5 }] } ]); }); it('with mixed symbols', () => { - const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD, ordersMixedSymbols); - const portfolioItemsAtTransactionPoints = portfolioCalculator.getPortfolioItemsAtTransactionPoints(); + const portfolioCalculator = new PortfolioCalculator(currentRateService, Currency.USD); + portfolioCalculator.computeTransactionPoints(ordersMixedSymbols); + const portfolioItemsAtTransactionPoints = portfolioCalculator.getTransactionPoints(); expect(portfolioItemsAtTransactionPoints).toEqual([ { @@ -346,7 +376,9 @@ describe('PortfolioCalculator', () => { quantity: new Big('50'), symbol: 'TSLA', investment: new Big('2148.5'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2017-01-03', + transactionCount: 1 }] }, { @@ -355,12 +387,16 @@ describe('PortfolioCalculator', () => { quantity: new Big('0.5614682'), symbol: 'BTCUSD', investment: new Big('1999.9999999999998659756'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2017-07-01', + transactionCount: 1 }, { quantity: new Big('50'), symbol: 'TSLA', investment: new Big('2148.5'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2017-01-03', + transactionCount: 1 }] }, { @@ -369,25 +405,55 @@ describe('PortfolioCalculator', () => { quantity: new Big('5'), symbol: 'AMZN', investment: new Big('10109.95'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2018-09-01', + transactionCount: 1 }, { quantity: new Big('0.5614682'), symbol: 'BTCUSD', investment: new Big('1999.9999999999998659756'), - currency: Currency.USD + currency: Currency.USD, + firstBuyDate: '2017-07-01', + transactionCount: 1 }, { quantity: new Big('50'), symbol: 'TSLA', investment: new Big('2148.5'), - currency: Currency.USD + 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); + portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints); + const currentPositions = await portfolioCalculator.getCurrentPositions(); + + expect(currentPositions).toEqual({ + // eslint-disable-next-line @typescript-eslint/naming-convention + VTI: { + averagePrice: new Big('178.438'), + firstBuyDate: '2019-02-01', + quantity: new Big('25'), + symbol: 'VTI', + investment: new Big('4460.95'), + marketPrice: new Big('213.32'), + transactionCount: 5 + } + }); + + + }) + + }) +}); const ordersMixedSymbols: PortfolioOrder[] = [ { date: '2017-01-03', @@ -457,3 +523,61 @@ const ordersVTI: PortfolioOrder[] = [ currency: Currency.USD } ]; + +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 + }] + }, + { + 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 + }] + }, + { + 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 + }] + }, + { + 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 + }] + }, + { + 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 + }] + } +]; diff --git a/apps/api/src/app/core/portfolio-calculator.ts b/apps/api/src/app/core/portfolio-calculator.ts index 4a10860e1..751d9a888 100644 --- a/apps/api/src/app/core/portfolio-calculator.ts +++ b/apps/api/src/app/core/portfolio-calculator.ts @@ -9,10 +9,8 @@ export class PortfolioCalculator { constructor( private currentRateService: CurrentRateService, - private currency: Currency, - orders: PortfolioOrder[] + private currency: Currency ) { - this.computeTransactionPoints(orders); } addOrder(order: PortfolioOrder): void { @@ -23,19 +21,7 @@ export class PortfolioCalculator { } - getPortfolioItemsAtTransactionPoints(): TransactionPoint[] { - return this.transactionPoints; - } - - getCurrentPositions(): { [symbol: string]: TimelinePosition } { - return {}; - } - - calculateTimeline(timelineSpecification: TimelineSpecification[], endDate: Date): TimelinePeriod[] { - return null; - } - - private computeTransactionPoints(orders: PortfolioOrder[]) { + computeTransactionPoints(orders: PortfolioOrder[]) { orders.sort((a, b) => a.date.localeCompare(b.date)); this.transactionPoints = []; @@ -56,14 +42,18 @@ export class PortfolioCalculator { quantity: order.quantity.mul(factor).plus(oldAccumulatedSymbol.quantity), symbol: order.symbol, investment: unitPrice.mul(order.quantity).mul(factor).add(oldAccumulatedSymbol.investment), - currency: order.currency + currency: order.currency, + firstBuyDate: oldAccumulatedSymbol.firstBuyDate, + transactionCount: oldAccumulatedSymbol.transactionCount + 1 }; } else { currentTransactionPointItem = { quantity: order.quantity.mul(factor), symbol: order.symbol, investment: unitPrice.mul(order.quantity).mul(factor), - currency: order.currency + currency: order.currency, + firstBuyDate: order.date, + transactionCount: 1 }; } @@ -90,6 +80,47 @@ export class PortfolioCalculator { } } + setTransactionPoints(transactionPoints: TransactionPoint[]) { + this.transactionPoints = transactionPoints; + } + + getTransactionPoints(): TransactionPoint[] { + return this.transactionPoints; + } + + async getCurrentPositions(): Promise<{ [symbol: string]: TimelinePosition }> { + if (!this.transactionPoints?.length) { + return {}; + } + + const lastTransactionPoint = this.transactionPoints[this.transactionPoints.length - 1]; + + const result: { [symbol: string]: TimelinePosition } = {}; + for (const item of lastTransactionPoint.items) { + const marketPrice = await this.currentRateService.getValue({ + date: new Date(), + symbol: item.symbol, + currency: item.currency, + userCurrency: this.currency + }); + result[item.symbol] = { + averagePrice: item.investment.div(item.quantity), + firstBuyDate: item.firstBuyDate, + quantity: item.quantity, + symbol: item.symbol, + investment: item.investment, + marketPrice: marketPrice, + transactionCount: item.transactionCount + }; + } + + return result; + } + + calculateTimeline(timelineSpecification: TimelineSpecification[], endDate: Date): TimelinePeriod[] { + return null; + } + private getFactor(type: OrderType) { let factor: number; switch (type) { @@ -118,6 +149,8 @@ interface TransactionPointSymbol { symbol: string; investment: Big; currency: Currency; + firstBuyDate: string; + transactionCount: number; } interface TimelinePosition {