From 4f7628921d3f8dacc92171cd9743cbc07709fcdc Mon Sep 17 00:00:00 2001 From: Valentin Zickner Date: Tue, 6 Jul 2021 20:54:51 +0200 Subject: [PATCH] add timeline time point calculation --- .../src/app/core/portfolio-calculator.spec.ts | 688 +++++++++++++++++- apps/api/src/app/core/portfolio-calculator.ts | 81 ++- 2 files changed, 758 insertions(+), 11 deletions(-) diff --git a/apps/api/src/app/core/portfolio-calculator.spec.ts b/apps/api/src/app/core/portfolio-calculator.spec.ts index d957ad54c..93106090f 100644 --- a/apps/api/src/app/core/portfolio-calculator.spec.ts +++ b/apps/api/src/app/core/portfolio-calculator.spec.ts @@ -1,6 +1,8 @@ import { PortfolioCalculator, - PortfolioOrder + PortfolioOrder, + TimelinePeriod, + TimelineSpecification } from '@ghostfolio/api/app/core/portfolio-calculator'; import { CurrentRateService, @@ -535,6 +537,690 @@ describe('PortfolioCalculator', () => { }); }); }); + + describe('calculate timeline', () => { + it('with yearly', async () => { + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); + portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints); + const timelineSpecification: TimelineSpecification[] = [ + { + start: '2019-01-01', + accuracy: 'year' + } + ]; + const timeline: TimelinePeriod[] = + await portfolioCalculator.calculateTimeline( + timelineSpecification, + '2021-12-31' + ); + + expect(timeline).toEqual([ + { + date: '2019-01-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-01-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-01-01', + grossPerformance: 0, + investment: 0, + value: 0 + } + ]); + }); + + it('with monthly', async () => { + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); + portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints); + const timelineSpecification: TimelineSpecification[] = [ + { + start: '2019-01-01', + accuracy: 'month' + } + ]; + const timeline: TimelinePeriod[] = + await portfolioCalculator.calculateTimeline( + timelineSpecification, + '2021-12-31' + ); + + expect(timeline).toEqual([ + { + date: '2019-01-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2019-02-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2019-03-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2019-04-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2019-05-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2019-06-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2019-07-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2019-08-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2019-09-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2019-10-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2019-11-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2019-12-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-01-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-02-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-03-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-04-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-05-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-06-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-07-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-08-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-09-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-10-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-11-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-12-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-01-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-02-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-03-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-04-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-05-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-06-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-07-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-08-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-09-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-10-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-11-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-01', + grossPerformance: 0, + investment: 0, + value: 0 + } + ]); + }); + + it('with yearly and monthly mixed', async () => { + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); + portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints); + const timelineSpecification: TimelineSpecification[] = [ + { + start: '2019-01-01', + accuracy: 'year' + }, + { + start: '2021-01-01', + accuracy: 'month' + } + ]; + const timeline: TimelinePeriod[] = + await portfolioCalculator.calculateTimeline( + timelineSpecification, + '2021-12-31' + ); + + expect(timeline).toEqual([ + { + date: '2019-01-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-01-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-01-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-02-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-03-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-04-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-05-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-06-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-07-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-08-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-09-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-10-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-11-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-01', + grossPerformance: 0, + investment: 0, + value: 0 + } + ]); + }); + + it('with all mixed', async () => { + const portfolioCalculator = new PortfolioCalculator( + currentRateService, + Currency.USD + ); + portfolioCalculator.setTransactionPoints(ordersVTITransactionPoints); + const timelineSpecification: TimelineSpecification[] = [ + { + start: '2019-01-01', + accuracy: 'year' + }, + { + start: '2021-01-01', + accuracy: 'month' + }, + { + start: '2021-12-01', + accuracy: 'day' + } + ]; + const timeline: TimelinePeriod[] = + await portfolioCalculator.calculateTimeline( + timelineSpecification, + '2021-12-31' + ); + + expect(timeline).toEqual([ + { + date: '2019-01-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2020-01-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-01-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-02-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-03-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-04-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-05-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-06-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-07-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-08-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-09-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-10-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-11-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-01', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-02', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-03', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-04', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-05', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-06', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-07', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-08', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-09', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-10', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-11', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-12', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-13', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-14', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-15', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-16', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-17', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-18', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-19', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-20', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-21', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-22', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-23', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-24', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-25', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-26', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-27', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-28', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-29', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-30', + grossPerformance: 0, + investment: 0, + value: 0 + }, + { + date: '2021-12-31', + grossPerformance: 0, + investment: 0, + value: 0 + } + ]); + }); + }); }); const ordersMixedSymbols: PortfolioOrder[] = [ { diff --git a/apps/api/src/app/core/portfolio-calculator.ts b/apps/api/src/app/core/portfolio-calculator.ts index c166a39d5..2a6fb52b1 100644 --- a/apps/api/src/app/core/portfolio-calculator.ts +++ b/apps/api/src/app/core/portfolio-calculator.ts @@ -2,6 +2,17 @@ import { Currency } from '@prisma/client'; import { CurrentRateService } from '@ghostfolio/api/app/core/current-rate.service'; import { OrderType } from '@ghostfolio/api/models/order-type'; import Big from 'big.js'; +import { + addDays, + addMonths, + addYears, + format, + isAfter, + isBefore, + parse +} from 'date-fns'; + +const DATE_FORMAT = 'yyyy-MM-dd'; export class PortfolioCalculator { private transactionPoints: TransactionPoint[]; @@ -11,10 +22,6 @@ export class PortfolioCalculator { private currency: Currency ) {} - addOrder(order: PortfolioOrder): void {} - - deleteOrder(order: PortfolioOrder): void {} - computeTransactionPoints(orders: PortfolioOrder[]) { orders.sort((a, b) => a.date.localeCompare(b.date)); @@ -121,9 +128,38 @@ export class PortfolioCalculator { calculateTimeline( timelineSpecification: TimelineSpecification[], - endDate: Date + endDate: string ): TimelinePeriod[] { - return null; + if (timelineSpecification.length === 0) { + return []; + } + + const startDate = timelineSpecification[0].start; + const start = parse(startDate, DATE_FORMAT, new Date()); + const end = parse(endDate, DATE_FORMAT, new Date()); + + const timelinePeriod: TimelinePeriod[] = []; + let i = 0; + for ( + let currentDate = start; + !isAfter(currentDate, end); + currentDate = this.addToDate( + currentDate, + timelineSpecification[i].accuracy + ) + ) { + if (this.isNextItemActive(timelineSpecification, currentDate, i)) { + i++; + } + timelinePeriod.push({ + date: format(currentDate, DATE_FORMAT), + grossPerformance: 0, + investment: 0, + value: 0 + }); + } + + return timelinePeriod; } private getFactor(type: OrderType) { @@ -141,6 +177,31 @@ export class PortfolioCalculator { } return factor; } + + private addToDate(date: Date, accurany: Accuracy): Date { + switch (accurany) { + case 'day': + return addDays(date, 1); + case 'month': + return addMonths(date, 1); + case 'year': + return addYears(date, 1); + } + } + + private isNextItemActive( + timelineSpecification: TimelineSpecification[], + currentDate: Date, + i: number + ) { + return ( + i + 1 < timelineSpecification.length && + !isBefore( + currentDate, + parse(timelineSpecification[i + 1].start, DATE_FORMAT, new Date()) + ) + ); + } } interface TransactionPoint { @@ -169,13 +230,13 @@ interface TimelinePosition { type Accuracy = 'year' | 'month' | 'day'; -interface TimelineSpecification { - start: Date; +export interface TimelineSpecification { + start: string; accuracy: Accuracy; } -interface TimelinePeriod { - date: Date; +export interface TimelinePeriod { + date: string; grossPerformance: number; investment: number; value: number;