Browse Source

implement analysis of all-time high/low

pull/428/head
Valentin Zickner 4 years ago
committed by Thomas
parent
commit
e4a7bce855
  1. 6
      apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts
  2. 8
      apps/api/src/app/portfolio/interfaces/timeline-info.interface.ts
  3. 63
      apps/api/src/app/portfolio/portfolio-calculator.spec.ts
  4. 70
      apps/api/src/app/portfolio/portfolio-calculator.ts
  5. 8
      apps/api/src/app/portfolio/portfolio.controller.ts
  6. 36
      apps/api/src/app/portfolio/portfolio.service.ts

6
apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts

@ -21,6 +21,12 @@ export interface PortfolioPositionDetail {
transactionCount: number; transactionCount: number;
} }
export interface HistoricalDataContainer {
isAllTimeHigh: boolean;
isAllTimeLow: boolean;
items: HistoricalDataItem[];
}
export interface HistoricalDataItem { export interface HistoricalDataItem {
averagePrice?: number; averagePrice?: number;
date: string; date: string;

8
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[];
}

63
apps/api/src/app/portfolio/portfolio-calculator.spec.ts

@ -1502,11 +1502,11 @@ describe('PortfolioCalculator', () => {
accuracy: 'year' accuracy: 'year'
} }
]; ];
const timeline: TimelinePeriod[] = const timelineInfo = await portfolioCalculator.calculateTimeline(
await portfolioCalculator.calculateTimeline( timelineSpecification,
timelineSpecification, '2021-06-30'
'2021-06-30' );
); const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods;
expect(timeline).toEqual([ expect(timeline).toEqual([
{ {
@ -1622,11 +1622,11 @@ describe('PortfolioCalculator', () => {
accuracy: 'year' accuracy: 'year'
} }
]; ];
const timeline: TimelinePeriod[] = const timelineInfo = await portfolioCalculator.calculateTimeline(
await portfolioCalculator.calculateTimeline( timelineSpecification,
timelineSpecification, '2021-06-30'
'2021-06-30' );
); const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods;
expect(timeline).toEqual([ expect(timeline).toEqual([
{ {
@ -1665,11 +1665,11 @@ describe('PortfolioCalculator', () => {
accuracy: 'month' accuracy: 'month'
} }
]; ];
const timeline: TimelinePeriod[] = const timelineInfo = await portfolioCalculator.calculateTimeline(
await portfolioCalculator.calculateTimeline( timelineSpecification,
timelineSpecification, '2021-06-30'
'2021-06-30' );
); const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods;
expect(timeline).toEqual([ expect(timeline).toEqual([
{ {
@ -1883,6 +1883,9 @@ describe('PortfolioCalculator', () => {
value: new Big('3186.9') // 15 * (144.38 + days=851 * 0.08) 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 () => { it('with yearly and monthly mixed', async () => {
@ -1901,11 +1904,11 @@ describe('PortfolioCalculator', () => {
accuracy: 'month' accuracy: 'month'
} }
]; ];
const timeline: TimelinePeriod[] = const timelineInfo = await portfolioCalculator.calculateTimeline(
await portfolioCalculator.calculateTimeline( timelineSpecification,
timelineSpecification, '2021-06-30'
'2021-06-30' );
); const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods;
expect(timeline).toEqual([ expect(timeline).toEqual([
{ {
@ -1987,11 +1990,11 @@ describe('PortfolioCalculator', () => {
accuracy: 'day' accuracy: 'day'
} }
]; ];
const timeline: TimelinePeriod[] = const timelineInfo = await portfolioCalculator.calculateTimeline(
await portfolioCalculator.calculateTimeline( timelineSpecification,
timelineSpecification, '2021-06-30'
'2021-06-30' );
); const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods;
expect(timeline).toEqual( expect(timeline).toEqual(
expect.objectContaining([ expect.objectContaining([
@ -2296,11 +2299,11 @@ describe('PortfolioCalculator', () => {
accuracy: 'year' accuracy: 'year'
} }
]; ];
const timeline: TimelinePeriod[] = const timelineInfo = await portfolioCalculator.calculateTimeline(
await portfolioCalculator.calculateTimeline( timelineSpecification,
timelineSpecification, '2020-01-01'
'2020-01-01' );
); const timeline: TimelinePeriod[] = timelineInfo.timelinePeriods;
expect(timeline).toEqual([ expect(timeline).toEqual([
{ {

70
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -29,6 +29,7 @@ import {
} from './interfaces/timeline-specification.interface'; } from './interfaces/timeline-specification.interface';
import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface'; import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from './interfaces/transaction-point.interface'; import { TransactionPoint } from './interfaces/transaction-point.interface';
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
export class PortfolioCalculator { export class PortfolioCalculator {
private transactionPoints: TransactionPoint[]; private transactionPoints: TransactionPoint[];
@ -365,16 +366,20 @@ export class PortfolioCalculator {
public async calculateTimeline( public async calculateTimeline(
timelineSpecification: TimelineSpecification[], timelineSpecification: TimelineSpecification[],
endDate: string endDate: string
): Promise<TimelinePeriod[]> { ): Promise<TimelineInfoInterface> {
if (timelineSpecification.length === 0) { if (timelineSpecification.length === 0) {
return []; return {
timelinePeriods: [],
maxNetPerformance: new Big(0),
minNetPerformance: new Big(0)
};
} }
const startDate = timelineSpecification[0].start; const startDate = timelineSpecification[0].start;
const start = parseDate(startDate); const start = parseDate(startDate);
const end = parseDate(endDate); const end = parseDate(endDate);
const timelinePeriodPromises: Promise<TimelinePeriod[]>[] = []; const timelinePeriodPromises: Promise<TimelineInfoInterface>[] = [];
let i = 0; let i = 0;
let j = -1; let j = -1;
for ( for (
@ -417,11 +422,38 @@ export class PortfolioCalculator {
} }
} }
const timelinePeriods: TimelinePeriod[][] = await Promise.all( const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all(
timelinePeriodPromises 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( private calculateOverallPerformance(
@ -513,7 +545,7 @@ export class PortfolioCalculator {
j: number, j: number,
startDate: Date, startDate: Date,
endDate: Date endDate: Date
): Promise<TimelinePeriod[]> { ): Promise<TimelineInfoInterface> {
let investment: Big = new Big(0); let investment: Big = new Big(0);
let fees: Big = new Big(0); let fees: Big = new Big(0);
@ -569,6 +601,8 @@ export class PortfolioCalculator {
} }
const results: TimelinePeriod[] = []; const results: TimelinePeriod[] = [];
let maxNetPerformance: Big = null;
let minNetPerformance: Big = null;
for ( for (
let currentDate = startDate; let currentDate = startDate;
isBefore(currentDate, endDate); isBefore(currentDate, endDate);
@ -592,18 +626,36 @@ export class PortfolioCalculator {
} }
if (!invalid) { if (!invalid) {
const grossPerformance = value.minus(investment); 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 = { const result = {
grossPerformance, grossPerformance,
netPerformance,
investment, investment,
value, value,
date: currentDateAsString, date: currentDateAsString
netPerformance: grossPerformance.minus(fees)
}; };
results.push(result); results.push(result);
} }
} }
return results; return {
maxNetPerformance,
minNetPerformance,
timelinePeriods: results
};
} }
private getFactor(type: OrderType) { private getFactor(type: OrderType) {

8
apps/api/src/app/portfolio/portfolio.controller.ts

@ -91,11 +91,13 @@ export class PortfolioController {
@Query('range') range, @Query('range') range,
@Res() res: Response @Res() res: Response
): Promise<PortfolioChart> { ): Promise<PortfolioChart> {
let chartData = await this.portfolioService.getChart( const historicalDataContainer = await this.portfolioService.getChart(
impersonationId, impersonationId,
range range
); );
let chartData = historicalDataContainer.items;
let hasNullValue = false; let hasNullValue = false;
chartData.forEach((chartDataItem) => { chartData.forEach((chartDataItem) => {
@ -129,8 +131,8 @@ export class PortfolioController {
} }
return <any>res.json({ return <any>res.json({
isAllTimeHigh: true, isAllTimeHigh: historicalDataContainer.isAllTimeHigh,
isAllTimeLow: false, isAllTimeLow: historicalDataContainer.isAllTimeLow,
chart: chartData chart: chartData
}); });
} }

36
apps/api/src/app/portfolio/portfolio.service.ts

@ -62,6 +62,7 @@ import {
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import {
HistoricalDataContainer,
HistoricalDataItem, HistoricalDataItem,
PortfolioPositionDetail PortfolioPositionDetail
} from './interfaces/portfolio-position-detail.interface'; } from './interfaces/portfolio-position-detail.interface';
@ -164,7 +165,7 @@ export class PortfolioService {
public async getChart( public async getChart(
aImpersonationId: string, aImpersonationId: string,
aDateRange: DateRange = 'max' aDateRange: DateRange = 'max'
): Promise<HistoricalDataItem[]> { ): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const portfolioCalculator = new PortfolioCalculator( const portfolioCalculator = new PortfolioCalculator(
@ -175,7 +176,11 @@ export class PortfolioService {
const { transactionPoints } = await this.getTransactionPoints({ userId }); const { transactionPoints } = await this.getTransactionPoints({ userId });
portfolioCalculator.setTransactionPoints(transactionPoints); portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) { if (transactionPoints.length === 0) {
return []; return {
isAllTimeHigh: false,
isAllTimeLow: false,
items: []
};
} }
let portfolioStart = parse( let portfolioStart = parse(
transactionPoints[0].date, transactionPoints[0].date,
@ -191,18 +196,41 @@ export class PortfolioService {
} }
]; ];
const timeline = await portfolioCalculator.calculateTimeline( const timelineInfo = await portfolioCalculator.calculateTimeline(
timelineSpecification, timelineSpecification,
format(new Date(), DATE_FORMAT) format(new Date(), DATE_FORMAT)
); );
return timeline const timeline = timelineInfo.timelinePeriods;
const items = timeline
.filter((timelineItem) => timelineItem !== null) .filter((timelineItem) => timelineItem !== null)
.map((timelineItem) => ({ .map((timelineItem) => ({
date: timelineItem.date, date: timelineItem.date,
marketPrice: timelineItem.value, marketPrice: timelineItem.value,
value: timelineItem.netPerformance.toNumber() 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( public async getDetails(

Loading…
Cancel
Save