Feature/accentuate all time high and low (#428)

* Accentuate all time high and all time low

* Update changelog

Co-authored-by: Valentin Zickner <ghostfolio@zickner.ch>
pull/487/head
Thomas Kaul 3 years ago committed by GitHub
parent 572bfc59b8
commit 1c2ca5b96b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Accentuated the all time high and the all time low
## 1.79.0 - 21.11.2021
### Added

@ -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;

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

@ -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([
{

@ -1,3 +1,4 @@
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
import { OrderType } from '@ghostfolio/api/models/order-type';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
@ -365,16 +366,20 @@ export class PortfolioCalculator {
public async calculateTimeline(
timelineSpecification: TimelineSpecification[],
endDate: string
): Promise<TimelinePeriod[]> {
): Promise<TimelineInfoInterface> {
if (timelineSpecification.length === 0) {
return [];
return {
maxNetPerformance: new Big(0),
minNetPerformance: new Big(0),
timelinePeriods: []
};
}
const startDate = timelineSpecification[0].start;
const start = parseDate(startDate);
const end = parseDate(endDate);
const timelinePeriodPromises: Promise<TimelinePeriod[]>[] = [];
const timelinePeriodPromises: Promise<TimelineInfoInterface>[] = [];
let i = 0;
let j = -1;
for (
@ -417,11 +422,40 @@ export class PortfolioCalculator {
}
}
const timelinePeriods: TimelinePeriod[][] = await Promise.all(
const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all(
timelinePeriodPromises
);
const minNetPerformance = timelineInfoInterfaces
.map((timelineInfo) => timelineInfo.minNetPerformance)
.filter((performance) => performance !== null)
.reduce((minPerformance, current) => {
if (minPerformance.lt(current)) {
return minPerformance;
} else {
return current;
}
});
return flatten(timelinePeriods);
const maxNetPerformance = timelineInfoInterfaces
.map((timelineInfo) => timelineInfo.maxNetPerformance)
.filter((performance) => performance !== null)
.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 +547,7 @@ export class PortfolioCalculator {
j: number,
startDate: Date,
endDate: Date
): Promise<TimelinePeriod[]> {
): Promise<TimelineInfoInterface> {
let investment: Big = new Big(0);
let fees: Big = new Big(0);
@ -569,6 +603,8 @@ export class PortfolioCalculator {
}
const results: TimelinePeriod[] = [];
let maxNetPerformance: Big = null;
let minNetPerformance: Big = null;
for (
let currentDate = startDate;
isBefore(currentDate, endDate);
@ -592,18 +628,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,
investment,
netPerformance,
value,
date: currentDateAsString,
netPerformance: grossPerformance.minus(fees)
date: currentDateAsString
};
results.push(result);
}
}
return results;
return {
maxNetPerformance,
minNetPerformance,
timelinePeriods: results
};
}
private getFactor(type: OrderType) {

@ -8,6 +8,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { baseCurrency } from '@ghostfolio/common/config';
import {
PortfolioChart,
PortfolioDetails,
PortfolioPerformance,
PortfolioPublicDetails,
@ -32,10 +33,7 @@ import { AuthGuard } from '@nestjs/passport';
import { Response } from 'express';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import {
HistoricalDataItem,
PortfolioPositionDetail
} from './interfaces/portfolio-position-detail.interface';
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
import { PortfolioPositions } from './interfaces/portfolio-positions.interface';
import { PortfolioService } from './portfolio.service';
@ -92,12 +90,14 @@ export class PortfolioController {
@Headers('impersonation-id') impersonationId,
@Query('range') range,
@Res() res: Response
): Promise<HistoricalDataItem[]> {
let chartData = await this.portfolioService.getChart(
): Promise<PortfolioChart> {
const historicalDataContainer = await this.portfolioService.getChart(
impersonationId,
range
);
let chartData = historicalDataContainer.items;
let hasNullValue = false;
chartData.forEach((chartDataItem) => {
@ -130,7 +130,11 @@ export class PortfolioController {
});
}
return <any>res.json(chartData);
return <any>res.json({
chart: chartData,
isAllTimeHigh: historicalDataContainer.isAllTimeHigh,
isAllTimeLow: historicalDataContainer.isAllTimeLow
});
}
@Get('details')

@ -56,12 +56,14 @@ import {
parse,
parseISO,
setDayOfYear,
startOfDay,
subDays,
subYears
} from 'date-fns';
import { isEmpty } from 'lodash';
import {
HistoricalDataContainer,
HistoricalDataItem,
PortfolioPositionDetail
} from './interfaces/portfolio-position-detail.interface';
@ -164,7 +166,7 @@ export class PortfolioService {
public async getChart(
aImpersonationId: string,
aDateRange: DateRange = 'max'
): Promise<HistoricalDataItem[]> {
): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const portfolioCalculator = new PortfolioCalculator(
@ -175,14 +177,21 @@ 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,
DATE_FORMAT,
new Date()
);
portfolioStart = this.getStartDate(aDateRange, portfolioStart);
// Get start date for the full portfolio because of because of the
// min and max calculation
portfolioStart = this.getStartDate('max', portfolioStart);
const timelineSpecification: TimelineSpecification[] = [
{
@ -191,18 +200,52 @@ 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;
}
portfolioStart = startOfDay(
this.getStartDate(
aDateRange,
parse(transactionPoints[0].date, DATE_FORMAT, new Date())
)
);
return {
isAllTimeHigh,
isAllTimeLow,
items: items.filter((item) => {
// Filter items of date range
return !isAfter(portfolioStart, parseDate(item.date));
})
};
}
public async getDetails(
@ -639,7 +682,9 @@ export class PortfolioService {
currentGrossPerformancePercent: 0,
currentNetPerformance: 0,
currentNetPerformancePercent: 0,
currentValue: 0
currentValue: 0,
isAllTimeHigh: false,
isAllTimeLow: false
}
};
}
@ -672,7 +717,9 @@ export class PortfolioService {
currentGrossPerformancePercent,
currentNetPerformance,
currentNetPerformancePercent,
currentValue
currentValue,
isAllTimeHigh: true, // TODO
isAllTimeLow: false // TODO
}
};
}

@ -1,5 +1,11 @@
<div class="container p-0">
<div class="row no-gutters">
<div
class="no-gutters row"
[ngClass]="{
'text-danger': isAllTimeLow,
'text-success': isAllTimeHigh
}"
>
<div class="flex-grow-1"></div>
<div *ngIf="isLoading" class="align-items-center d-flex">
<ngx-skeleton-loader
@ -12,8 +18,8 @@
></ngx-skeleton-loader>
</div>
<div
[hidden]="isLoading"
class="display-4 font-weight-bold m-0 text-center value-container"
[hidden]="isLoading"
>
<span #value id="value"></span>
</div>

@ -19,6 +19,8 @@ import { isNumber } from 'lodash';
})
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
@Input() baseCurrency: string;
@Input() isAllTimeHigh: boolean;
@Input() isAllTimeLow: boolean;
@Input() isLoading: boolean;
@Input() locale: string;
@Input() performance: PortfolioPerformance;

@ -59,6 +59,8 @@ export class HomePageComponent implements OnDestroy, OnInit {
public hasPermissionToAccessFearAndGreedIndex: boolean;
public hasPermissionToCreateOrder: boolean;
public historicalDataItems: LineChartItem[];
public isAllTimeHigh: boolean;
public isAllTimeLow: boolean;
public isLoadingPerformance = true;
public isLoadingSummary = true;
public performance: PortfolioPerformance;
@ -166,12 +168,14 @@ export class HomePageComponent implements OnDestroy, OnInit {
.fetchChart({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.map((chartDataItem) => {
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
});
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});

@ -54,6 +54,8 @@
<gf-portfolio-performance
class="pb-4"
[baseCurrency]="user?.settings?.baseCurrency"
[isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[performance]="performance"

@ -39,6 +39,8 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean;
public historicalDataItems: LineChartItem[];
public isAllTimeHigh: boolean;
public isAllTimeLow: boolean;
public isLoadingPerformance = true;
public performance: PortfolioPerformance;
public positions: Position[];
@ -114,12 +116,14 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
.fetchChart({ range: this.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.map((chartDataItem) => {
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
});
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});

@ -47,6 +47,8 @@
<gf-portfolio-performance
class="pb-4"
[baseCurrency]="user?.settings?.baseCurrency"
[isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[performance]="performance"

@ -5,10 +5,7 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import {
HistoricalDataItem,
PortfolioPositionDetail
} from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
import { PortfolioPositionDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
@ -21,6 +18,7 @@ import {
AdminData,
Export,
InfoItem,
PortfolioChart,
PortfolioDetails,
PortfolioPerformance,
PortfolioPublicDetails,
@ -30,12 +28,8 @@ import {
} from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { permissions } from '@ghostfolio/common/permissions';
import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
import {
Account as AccountModel,
DataSource,
Order as OrderModel
} from '@prisma/client';
import { DateRange } from '@ghostfolio/common/types';
import { DataSource, Order as OrderModel } from '@prisma/client';
import { parseISO } from 'date-fns';
import { cloneDeep } from 'lodash';
import { Observable } from 'rxjs';
@ -91,7 +85,7 @@ export class DataService {
}
public fetchChart({ range }: { range: DateRange }) {
return this.http.get<HistoricalDataItem[]>('/api/portfolio/chart', {
return this.http.get<PortfolioChart>('/api/portfolio/chart', {
params: { range }
});
}

@ -3,6 +3,7 @@ import { Accounts } from './accounts.interface';
import { AdminData } from './admin-data.interface';
import { Export } from './export.interface';
import { InfoItem } from './info-item.interface';
import { PortfolioChart } from './portfolio-chart.interface';
import { PortfolioDetails } from './portfolio-details.interface';
import { PortfolioItem } from './portfolio-item.interface';
import { PortfolioOverview } from './portfolio-overview.interface';
@ -24,6 +25,7 @@ export {
AdminData,
Export,
InfoItem,
PortfolioChart,
PortfolioDetails,
PortfolioItem,
PortfolioOverview,

@ -0,0 +1,7 @@
import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
export interface PortfolioChart {
isAllTimeHigh: boolean;
isAllTimeLow: boolean;
chart: HistoricalDataItem[];
}

@ -5,4 +5,6 @@ export interface PortfolioPerformance {
currentNetPerformance: number;
currentNetPerformancePercent: number;
currentValue: number;
isAllTimeHigh: boolean;
isAllTimeLow: boolean;
}

Loading…
Cancel
Save