From e4a7bce855ad03bed9a08b1d304f694b0586cd51 Mon Sep 17 00:00:00 2001 From: Valentin Zickner Date: Wed, 20 Oct 2021 21:38:35 +0200 Subject: [PATCH] implement analysis of all-time high/low --- .../portfolio-position-detail.interface.ts | 6 ++ .../interfaces/timeline-info.interface.ts | 8 +++ .../portfolio/portfolio-calculator.spec.ts | 63 +++++++++-------- .../src/app/portfolio/portfolio-calculator.ts | 70 ++++++++++++++++--- .../src/app/portfolio/portfolio.controller.ts | 8 ++- .../src/app/portfolio/portfolio.service.ts | 36 ++++++++-- 6 files changed, 145 insertions(+), 46 deletions(-) create mode 100644 apps/api/src/app/portfolio/interfaces/timeline-info.interface.ts diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts index c3824802c..336e5a2d4 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts @@ -21,6 +21,12 @@ export interface PortfolioPositionDetail { transactionCount: number; } +export interface HistoricalDataContainer { + isAllTimeHigh: boolean; + isAllTimeLow: boolean; + items: HistoricalDataItem[]; +} + export interface HistoricalDataItem { averagePrice?: number; date: string; diff --git a/apps/api/src/app/portfolio/interfaces/timeline-info.interface.ts b/apps/api/src/app/portfolio/interfaces/timeline-info.interface.ts new file mode 100644 index 000000000..7fb7db06a --- /dev/null +++ b/apps/api/src/app/portfolio/interfaces/timeline-info.interface.ts @@ -0,0 +1,8 @@ +import { TimelinePeriod } from '@ghostfolio/api/app/portfolio/interfaces/timeline-period.interface'; +import Big from 'big.js'; + +export interface TimelineInfoInterface { + maxNetPerformance: Big; + minNetPerformance: Big; + timelinePeriods: TimelinePeriod[]; +} diff --git a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts index 5dd8bb62a..1e3d6b576 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.spec.ts @@ -1502,11 +1502,11 @@ describe('PortfolioCalculator', () => { accuracy: 'year' } ]; - const timeline: TimelinePeriod[] = - await portfolioCalculator.calculateTimeline( - timelineSpecification, - '2021-06-30' - ); + const timelineInfo = await portfolioCalculator.calculateTimeline( + timelineSpecification, + '2021-06-30' + ); + const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods; expect(timeline).toEqual([ { @@ -1622,11 +1622,11 @@ describe('PortfolioCalculator', () => { accuracy: 'year' } ]; - const timeline: TimelinePeriod[] = - await portfolioCalculator.calculateTimeline( - timelineSpecification, - '2021-06-30' - ); + const timelineInfo = await portfolioCalculator.calculateTimeline( + timelineSpecification, + '2021-06-30' + ); + const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods; expect(timeline).toEqual([ { @@ -1665,11 +1665,11 @@ describe('PortfolioCalculator', () => { accuracy: 'month' } ]; - const timeline: TimelinePeriod[] = - await portfolioCalculator.calculateTimeline( - timelineSpecification, - '2021-06-30' - ); + const timelineInfo = await portfolioCalculator.calculateTimeline( + timelineSpecification, + '2021-06-30' + ); + const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods; expect(timeline).toEqual([ { @@ -1883,6 +1883,9 @@ describe('PortfolioCalculator', () => { value: new Big('3186.9') // 15 * (144.38 + days=851 * 0.08) } ]); + + expect(timelineInfo.maxNetPerformance).toEqual(new Big('547.9')); + expect(timelineInfo.minNetPerformance).toEqual(new Big('0')); }); it('with yearly and monthly mixed', async () => { @@ -1901,11 +1904,11 @@ describe('PortfolioCalculator', () => { accuracy: 'month' } ]; - const timeline: TimelinePeriod[] = - await portfolioCalculator.calculateTimeline( - timelineSpecification, - '2021-06-30' - ); + const timelineInfo = await portfolioCalculator.calculateTimeline( + timelineSpecification, + '2021-06-30' + ); + const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods; expect(timeline).toEqual([ { @@ -1987,11 +1990,11 @@ describe('PortfolioCalculator', () => { accuracy: 'day' } ]; - const timeline: TimelinePeriod[] = - await portfolioCalculator.calculateTimeline( - timelineSpecification, - '2021-06-30' - ); + const timelineInfo = await portfolioCalculator.calculateTimeline( + timelineSpecification, + '2021-06-30' + ); + const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods; expect(timeline).toEqual( expect.objectContaining([ @@ -2296,11 +2299,11 @@ describe('PortfolioCalculator', () => { accuracy: 'year' } ]; - const timeline: TimelinePeriod[] = - await portfolioCalculator.calculateTimeline( - timelineSpecification, - '2020-01-01' - ); + const timelineInfo = await portfolioCalculator.calculateTimeline( + timelineSpecification, + '2020-01-01' + ); + const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods; expect(timeline).toEqual([ { diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index cc2f1c4ed..149f02cea 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -29,6 +29,7 @@ import { } from './interfaces/timeline-specification.interface'; import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface'; import { TransactionPoint } from './interfaces/transaction-point.interface'; +import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface'; export class PortfolioCalculator { private transactionPoints: TransactionPoint[]; @@ -365,16 +366,20 @@ export class PortfolioCalculator { public async calculateTimeline( timelineSpecification: TimelineSpecification[], endDate: string - ): Promise { + ): Promise { if (timelineSpecification.length === 0) { - return []; + return { + timelinePeriods: [], + maxNetPerformance: new Big(0), + minNetPerformance: new Big(0) + }; } const startDate = timelineSpecification[0].start; const start = parseDate(startDate); const end = parseDate(endDate); - const timelinePeriodPromises: Promise[] = []; + const timelinePeriodPromises: Promise[] = []; let i = 0; let j = -1; for ( @@ -417,11 +422,38 @@ export class PortfolioCalculator { } } - const timelinePeriods: TimelinePeriod[][] = await Promise.all( + const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all( timelinePeriodPromises ); + const minNetPerformance = timelineInfoInterfaces + .map((timelineInfo) => timelineInfo.minNetPerformance) + .reduce((minPerformance, current) => { + if (minPerformance.lt(current)) { + return minPerformance; + } else { + return current; + } + }); - return flatten(timelinePeriods); + const maxNetPerformance = timelineInfoInterfaces + .map((timelineInfo) => timelineInfo.maxNetPerformance) + .reduce((maxPerformance, current) => { + if (maxPerformance.gt(current)) { + return maxPerformance; + } else { + return current; + } + }); + + const timelinePeriods = timelineInfoInterfaces.map( + (timelineInfo) => timelineInfo.timelinePeriods + ); + + return { + maxNetPerformance, + minNetPerformance, + timelinePeriods: flatten(timelinePeriods) + }; } private calculateOverallPerformance( @@ -513,7 +545,7 @@ export class PortfolioCalculator { j: number, startDate: Date, endDate: Date - ): Promise { + ): Promise { let investment: Big = new Big(0); let fees: Big = new Big(0); @@ -569,6 +601,8 @@ export class PortfolioCalculator { } const results: TimelinePeriod[] = []; + let maxNetPerformance: Big = null; + let minNetPerformance: Big = null; for ( let currentDate = startDate; isBefore(currentDate, endDate); @@ -592,18 +626,36 @@ export class PortfolioCalculator { } if (!invalid) { const grossPerformance = value.minus(investment); + const netPerformance = grossPerformance.minus(fees); + if ( + minNetPerformance === null || + minNetPerformance.gt(netPerformance) + ) { + minNetPerformance = netPerformance; + } + if ( + maxNetPerformance === null || + maxNetPerformance.lt(netPerformance) + ) { + maxNetPerformance = netPerformance; + } + const result = { grossPerformance, + netPerformance, investment, value, - date: currentDateAsString, - netPerformance: grossPerformance.minus(fees) + date: currentDateAsString }; results.push(result); } } - return results; + return { + maxNetPerformance, + minNetPerformance, + timelinePeriods: results + }; } private getFactor(type: OrderType) { diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 01b58ad5a..4bf4a62cf 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -91,11 +91,13 @@ export class PortfolioController { @Query('range') range, @Res() res: Response ): Promise { - let chartData = await this.portfolioService.getChart( + const historicalDataContainer = await this.portfolioService.getChart( impersonationId, range ); + let chartData = historicalDataContainer.items; + let hasNullValue = false; chartData.forEach((chartDataItem) => { @@ -129,8 +131,8 @@ export class PortfolioController { } return res.json({ - isAllTimeHigh: true, - isAllTimeLow: false, + isAllTimeHigh: historicalDataContainer.isAllTimeHigh, + isAllTimeLow: historicalDataContainer.isAllTimeLow, chart: chartData }); } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b5656ab3a..427ad1e1e 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -62,6 +62,7 @@ import { import { isEmpty } from 'lodash'; import { + HistoricalDataContainer, HistoricalDataItem, PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface'; @@ -164,7 +165,7 @@ export class PortfolioService { public async getChart( aImpersonationId: string, aDateRange: DateRange = 'max' - ): Promise { + ): Promise { const userId = await this.getUserId(aImpersonationId, this.request.user.id); const portfolioCalculator = new PortfolioCalculator( @@ -175,7 +176,11 @@ export class PortfolioService { const { transactionPoints } = await this.getTransactionPoints({ userId }); portfolioCalculator.setTransactionPoints(transactionPoints); if (transactionPoints.length === 0) { - return []; + return { + isAllTimeHigh: false, + isAllTimeLow: false, + items: [] + }; } let portfolioStart = parse( transactionPoints[0].date, @@ -191,18 +196,41 @@ export class PortfolioService { } ]; - const timeline = await portfolioCalculator.calculateTimeline( + const timelineInfo = await portfolioCalculator.calculateTimeline( timelineSpecification, format(new Date(), DATE_FORMAT) ); - return timeline + const timeline = timelineInfo.timelinePeriods; + + const items = timeline .filter((timelineItem) => timelineItem !== null) .map((timelineItem) => ({ date: timelineItem.date, marketPrice: timelineItem.value, value: timelineItem.netPerformance.toNumber() })); + + let lastItem = null; + if (timeline.length > 0) { + lastItem = timeline[timeline.length - 1]; + } + + let isAllTimeHigh = timelineInfo.maxNetPerformance?.eq( + lastItem?.netPerformance + ); + let isAllTimeLow = timelineInfo.minNetPerformance?.eq( + lastItem?.netPerformance + ); + if (isAllTimeHigh && isAllTimeLow) { + isAllTimeHigh = false; + isAllTimeLow = false; + } + return { + isAllTimeHigh, + isAllTimeLow, + items + }; } public async getDetails(