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>
This commit is contained in:
parent
572bfc59b8
commit
1c2ca5b96b
@ -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/),
|
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).
|
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
|
## 1.79.0 - 21.11.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -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;
|
||||||
|
@ -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'
|
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([
|
||||||
{
|
{
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
|
||||||
import { OrderType } from '@ghostfolio/api/models/order-type';
|
import { OrderType } from '@ghostfolio/api/models/order-type';
|
||||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||||
@ -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 {
|
||||||
|
maxNetPerformance: new Big(0),
|
||||||
|
minNetPerformance: new Big(0),
|
||||||
|
timelinePeriods: []
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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,40 @@ export class PortfolioCalculator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const timelinePeriods: TimelinePeriod[][] = await Promise.all(
|
const timelineInfoInterfaces: TimelineInfoInterface[] = await Promise.all(
|
||||||
timelinePeriodPromises
|
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(
|
private calculateOverallPerformance(
|
||||||
@ -513,7 +547,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 +603,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 +628,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,
|
||||||
investment,
|
investment,
|
||||||
|
netPerformance,
|
||||||
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,6 +8,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { baseCurrency } from '@ghostfolio/common/config';
|
import { baseCurrency } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
|
PortfolioChart,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
@ -32,10 +33,7 @@ import { AuthGuard } from '@nestjs/passport';
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
import {
|
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
|
||||||
HistoricalDataItem,
|
|
||||||
PortfolioPositionDetail
|
|
||||||
} from './interfaces/portfolio-position-detail.interface';
|
|
||||||
import { PortfolioPositions } from './interfaces/portfolio-positions.interface';
|
import { PortfolioPositions } from './interfaces/portfolio-positions.interface';
|
||||||
import { PortfolioService } from './portfolio.service';
|
import { PortfolioService } from './portfolio.service';
|
||||||
|
|
||||||
@ -92,12 +90,14 @@ export class PortfolioController {
|
|||||||
@Headers('impersonation-id') impersonationId,
|
@Headers('impersonation-id') impersonationId,
|
||||||
@Query('range') range,
|
@Query('range') range,
|
||||||
@Res() res: Response
|
@Res() res: Response
|
||||||
): Promise<HistoricalDataItem[]> {
|
): 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) => {
|
||||||
@ -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')
|
@Get('details')
|
||||||
|
@ -56,12 +56,14 @@ import {
|
|||||||
parse,
|
parse,
|
||||||
parseISO,
|
parseISO,
|
||||||
setDayOfYear,
|
setDayOfYear,
|
||||||
|
startOfDay,
|
||||||
subDays,
|
subDays,
|
||||||
subYears
|
subYears
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
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 +166,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,14 +177,21 @@ 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,
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
new Date()
|
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[] = [
|
const timelineSpecification: TimelineSpecification[] = [
|
||||||
{
|
{
|
||||||
@ -191,18 +200,52 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
public async getDetails(
|
||||||
@ -639,7 +682,9 @@ export class PortfolioService {
|
|||||||
currentGrossPerformancePercent: 0,
|
currentGrossPerformancePercent: 0,
|
||||||
currentNetPerformance: 0,
|
currentNetPerformance: 0,
|
||||||
currentNetPerformancePercent: 0,
|
currentNetPerformancePercent: 0,
|
||||||
currentValue: 0
|
currentValue: 0,
|
||||||
|
isAllTimeHigh: false,
|
||||||
|
isAllTimeLow: false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -672,7 +717,9 @@ export class PortfolioService {
|
|||||||
currentGrossPerformancePercent,
|
currentGrossPerformancePercent,
|
||||||
currentNetPerformance,
|
currentNetPerformance,
|
||||||
currentNetPerformancePercent,
|
currentNetPerformancePercent,
|
||||||
currentValue
|
currentValue,
|
||||||
|
isAllTimeHigh: true, // TODO
|
||||||
|
isAllTimeLow: false // TODO
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
<div class="container p-0">
|
<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 class="flex-grow-1"></div>
|
||||||
<div *ngIf="isLoading" class="align-items-center d-flex">
|
<div *ngIf="isLoading" class="align-items-center d-flex">
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
@ -12,8 +18,8 @@
|
|||||||
></ngx-skeleton-loader>
|
></ngx-skeleton-loader>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
[hidden]="isLoading"
|
|
||||||
class="display-4 font-weight-bold m-0 text-center value-container"
|
class="display-4 font-weight-bold m-0 text-center value-container"
|
||||||
|
[hidden]="isLoading"
|
||||||
>
|
>
|
||||||
<span #value id="value"></span>
|
<span #value id="value"></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,6 +19,8 @@ import { isNumber } from 'lodash';
|
|||||||
})
|
})
|
||||||
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
export class PortfolioPerformanceComponent implements OnChanges, OnInit {
|
||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
|
@Input() isAllTimeHigh: boolean;
|
||||||
|
@Input() isAllTimeLow: boolean;
|
||||||
@Input() isLoading: boolean;
|
@Input() isLoading: boolean;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() performance: PortfolioPerformance;
|
@Input() performance: PortfolioPerformance;
|
||||||
|
@ -59,6 +59,8 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
public hasPermissionToAccessFearAndGreedIndex: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
|
public isAllTimeHigh: boolean;
|
||||||
|
public isAllTimeLow: boolean;
|
||||||
public isLoadingPerformance = true;
|
public isLoadingPerformance = true;
|
||||||
public isLoadingSummary = true;
|
public isLoadingSummary = true;
|
||||||
public performance: PortfolioPerformance;
|
public performance: PortfolioPerformance;
|
||||||
@ -166,12 +168,14 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
.fetchChart({ range: this.dateRange })
|
.fetchChart({ range: this.dateRange })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((chartData) => {
|
.subscribe((chartData) => {
|
||||||
this.historicalDataItems = chartData.map((chartDataItem) => {
|
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
|
||||||
return {
|
return {
|
||||||
date: chartDataItem.date,
|
date: chartDataItem.date,
|
||||||
value: chartDataItem.value
|
value: chartDataItem.value
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
this.isAllTimeHigh = chartData.isAllTimeHigh;
|
||||||
|
this.isAllTimeLow = chartData.isAllTimeLow;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
@ -54,6 +54,8 @@
|
|||||||
<gf-portfolio-performance
|
<gf-portfolio-performance
|
||||||
class="pb-4"
|
class="pb-4"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[isAllTimeHigh]="isAllTimeHigh"
|
||||||
|
[isAllTimeLow]="isAllTimeLow"
|
||||||
[isLoading]="isLoadingPerformance"
|
[isLoading]="isLoadingPerformance"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[performance]="performance"
|
[performance]="performance"
|
||||||
|
@ -39,6 +39,8 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
|
public isAllTimeHigh: boolean;
|
||||||
|
public isAllTimeLow: boolean;
|
||||||
public isLoadingPerformance = true;
|
public isLoadingPerformance = true;
|
||||||
public performance: PortfolioPerformance;
|
public performance: PortfolioPerformance;
|
||||||
public positions: Position[];
|
public positions: Position[];
|
||||||
@ -114,12 +116,14 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
|
|||||||
.fetchChart({ range: this.dateRange })
|
.fetchChart({ range: this.dateRange })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((chartData) => {
|
.subscribe((chartData) => {
|
||||||
this.historicalDataItems = chartData.map((chartDataItem) => {
|
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
|
||||||
return {
|
return {
|
||||||
date: chartDataItem.date,
|
date: chartDataItem.date,
|
||||||
value: chartDataItem.value
|
value: chartDataItem.value
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
this.isAllTimeHigh = chartData.isAllTimeHigh;
|
||||||
|
this.isAllTimeLow = chartData.isAllTimeLow;
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
@ -47,6 +47,8 @@
|
|||||||
<gf-portfolio-performance
|
<gf-portfolio-performance
|
||||||
class="pb-4"
|
class="pb-4"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[isAllTimeHigh]="isAllTimeHigh"
|
||||||
|
[isAllTimeLow]="isAllTimeLow"
|
||||||
[isLoading]="isLoadingPerformance"
|
[isLoading]="isLoadingPerformance"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[performance]="performance"
|
[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 { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import {
|
import { PortfolioPositionDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
|
||||||
HistoricalDataItem,
|
|
||||||
PortfolioPositionDetail
|
|
||||||
} from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
|
|
||||||
import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface';
|
import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface';
|
||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
|
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
|
||||||
@ -21,6 +18,7 @@ import {
|
|||||||
AdminData,
|
AdminData,
|
||||||
Export,
|
Export,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
|
PortfolioChart,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
@ -30,12 +28,8 @@ import {
|
|||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
import {
|
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||||
Account as AccountModel,
|
|
||||||
DataSource,
|
|
||||||
Order as OrderModel
|
|
||||||
} from '@prisma/client';
|
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
@ -91,7 +85,7 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fetchChart({ range }: { range: DateRange }) {
|
public fetchChart({ range }: { range: DateRange }) {
|
||||||
return this.http.get<HistoricalDataItem[]>('/api/portfolio/chart', {
|
return this.http.get<PortfolioChart>('/api/portfolio/chart', {
|
||||||
params: { range }
|
params: { range }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { Accounts } from './accounts.interface';
|
|||||||
import { AdminData } from './admin-data.interface';
|
import { AdminData } from './admin-data.interface';
|
||||||
import { Export } from './export.interface';
|
import { Export } from './export.interface';
|
||||||
import { InfoItem } from './info-item.interface';
|
import { InfoItem } from './info-item.interface';
|
||||||
|
import { PortfolioChart } from './portfolio-chart.interface';
|
||||||
import { PortfolioDetails } from './portfolio-details.interface';
|
import { PortfolioDetails } from './portfolio-details.interface';
|
||||||
import { PortfolioItem } from './portfolio-item.interface';
|
import { PortfolioItem } from './portfolio-item.interface';
|
||||||
import { PortfolioOverview } from './portfolio-overview.interface';
|
import { PortfolioOverview } from './portfolio-overview.interface';
|
||||||
@ -24,6 +25,7 @@ export {
|
|||||||
AdminData,
|
AdminData,
|
||||||
Export,
|
Export,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
|
PortfolioChart,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
PortfolioItem,
|
PortfolioItem,
|
||||||
PortfolioOverview,
|
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;
|
currentNetPerformance: number;
|
||||||
currentNetPerformancePercent: number;
|
currentNetPerformancePercent: number;
|
||||||
currentValue: number;
|
currentValue: number;
|
||||||
|
isAllTimeHigh: boolean;
|
||||||
|
isAllTimeLow: boolean;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user