Browse Source

Add total amount chart

pull/1344/head
Thomas 3 years ago
parent
commit
4b57c6f539
  1. 23
      apps/api/src/app/portfolio/portfolio.controller.ts
  2. 116
      apps/api/src/app/portfolio/portfolio.service.ts
  3. 4
      apps/client/src/app/components/home-market/home-market.component.ts
  4. 2
      apps/client/src/app/components/home-market/home-market.html
  5. 14
      apps/client/src/app/components/home-overview/home-overview.component.ts
  6. 87
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  7. 27
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  8. 5
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  9. 24
      apps/client/src/app/services/data.service.ts
  10. 2
      libs/common/src/lib/interfaces/historical-data-item.interface.ts

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

@ -68,7 +68,7 @@ export class PortfolioController {
@Headers('impersonation-id') impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('range') range?: DateRange,
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string
): Promise<PortfolioDetails & { hasError: boolean }> {
let hasError = false;
@ -88,9 +88,9 @@ export class PortfolioController {
summary,
totalValueInBaseCurrency
} = await this.portfolioService.getDetails({
dateRange,
filters,
impersonationId,
dateRange: range,
userId: this.request.user.id
});
@ -183,6 +183,7 @@ export class PortfolioController {
@UseGuards(AuthGuard('jwt'))
public async getInvestments(
@Headers('impersonation-id') impersonationId: string,
@Query('range') dateRange: DateRange = 'max',
@Query('groupBy') groupBy?: GroupBy
): Promise<PortfolioInvestments> {
if (
@ -198,12 +199,16 @@ export class PortfolioController {
let investments: InvestmentItem[];
if (groupBy === 'month') {
investments = await this.portfolioService.getInvestments(
investments = await this.portfolioService.getInvestments({
dateRange,
impersonationId,
'month'
);
groupBy: 'month'
});
} else {
investments = await this.portfolioService.getInvestments(impersonationId);
investments = await this.portfolioService.getInvestments({
dateRange,
impersonationId
});
}
if (
@ -230,7 +235,7 @@ export class PortfolioController {
@Version('2')
public async getPerformanceV2(
@Headers('impersonation-id') impersonationId: string,
@Query('range') dateRange
@Query('range') dateRange: DateRange = 'max'
): Promise<PortfolioPerformanceResponse> {
const performanceInformation = await this.portfolioService.getPerformanceV2(
{
@ -258,11 +263,11 @@ export class PortfolioController {
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getPositions(
@Headers('impersonation-id') impersonationId: string,
@Query('range') range
@Query('range') dateRange: DateRange = 'max'
): Promise<PortfolioPositions> {
const result = await this.portfolioService.getPositions(
impersonationId,
range
dateRange
);
if (

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

@ -207,11 +207,16 @@ export class PortfolioService {
};
}
public async getInvestments(
aImpersonationId: string,
groupBy?: GroupBy
): Promise<InvestmentItem[]> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
public async getInvestments({
dateRange,
impersonationId,
groupBy
}: {
dateRange: DateRange;
impersonationId: string;
groupBy?: GroupBy;
}): Promise<InvestmentItem[]> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
@ -283,98 +288,18 @@ export class PortfolioService {
}
}
return sortBy(investments, (investment) => {
investments = sortBy(investments, (investment) => {
return investment.date;
});
}
public async getChart(
aImpersonationId: string,
aDateRange: DateRange = 'max'
): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
userId
});
const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
portfolioCalculator.setTransactionPoints(transactionPoints);
if (transactionPoints.length === 0) {
return {
isAllTimeHigh: false,
isAllTimeLow: false,
items: []
};
}
let portfolioStart = parse(
transactionPoints[0].date,
DATE_FORMAT,
new Date()
);
// Get start date for the full portfolio because of because of the
// min and max calculation
portfolioStart = this.getStartDate('max', portfolioStart);
const timelineSpecification: TimelineSpecification[] = [
{
start: format(portfolioStart, DATE_FORMAT),
accuracy: 'day'
}
];
const timelineInfo = await portfolioCalculator.calculateTimeline(
timelineSpecification,
format(new Date(), DATE_FORMAT)
);
const timeline = timelineInfo.timelinePeriods;
const items = timeline
.filter((timelineItem) => timelineItem !== null)
.map((timelineItem) => ({
date: timelineItem.date,
value: timelineItem.netPerformance.toNumber()
}));
let lastItem = null;
if (timeline.length > 0) {
lastItem = timeline[timeline.length - 1];
}
let isAllTimeHigh = timelineInfo.maxNetPerformance?.eq(
lastItem?.netPerformance ?? 0
);
let isAllTimeLow = timelineInfo.minNetPerformance?.eq(
lastItem?.netPerformance ?? 0
);
if (isAllTimeHigh && isAllTimeLow) {
isAllTimeHigh = false;
isAllTimeLow = false;
}
portfolioStart = startOfDay(
this.getStartDate(
aDateRange,
parse(transactionPoints[0].date, DATE_FORMAT, new Date())
)
const startDate = this.getStartDate(
dateRange,
parseDate(investments[0]?.date)
);
return {
isAllTimeHigh,
isAllTimeLow,
items: items.filter((item) => {
// Filter items of date range
return !isAfter(portfolioStart, parseDate(item.date));
})
};
return investments.filter(({ date }) => {
return !isBefore(parseDate(date), startDate);
});
}
public async getChartV2({
@ -441,7 +366,7 @@ export class PortfolioService {
filters?: Filter[];
withExcludedAccounts?: boolean;
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
// TODO:
// TODO
userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId });
@ -1035,10 +960,11 @@ export class PortfolioService {
return {
chart: historicalDataContainer.items.map(
({ date, netPerformanceInPercentage }) => {
({ date, netPerformance, netPerformanceInPercentage }) => {
return {
date,
value: netPerformanceInPercentage
netPerformance,
netPerformanceInPercentage
};
}
),

4
apps/client/src/app/components/home-market/home-market.component.ts

@ -24,7 +24,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
public fearLabel = $localize`Fear`;
public greedLabel = $localize`Greed`;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public historicalData: HistoricalDataItem[];
public historicalDataItems: HistoricalDataItem[];
public info: InfoItem;
public isLoading = true;
public readonly numberOfDays = 180;
@ -67,7 +67,7 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.historicalData = [
this.historicalDataItems = [
...historicalData,
{
date: resetHours(new Date()).toISOString(),

2
apps/client/src/app/components/home-market/home-market.html

@ -10,7 +10,7 @@
symbol="Fear & Greed Index"
yMax="100"
yMin="0"
[historicalDataItems]="historicalData"
[historicalDataItems]="historicalDataItems"
[isAnimated]="true"
[locale]="user?.settings?.locale"
[showXAxis]="true"

14
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -116,12 +116,14 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
this.performance = response.performance;
this.isLoadingPerformance = false;
this.historicalDataItems = response.chart.map(({ date, value }) => {
return {
date,
value
};
});
this.historicalDataItems = response.chart.map(
({ date, netPerformanceInPercentage }) => {
return {
date,
value: netPerformanceInPercentage
};
}
);
this.changeDetectorRef.markForCheck();
});

87
apps/client/src/app/components/investment-chart/investment-chart.component.ts

@ -15,6 +15,7 @@ import {
} from '@ghostfolio/common/chart-helper';
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
import {
DATE_FORMAT,
getBackgroundColor,
getDateFormatString,
getTextColor,
@ -22,7 +23,7 @@ import {
transformTickToAbbreviation
} from '@ghostfolio/common/helper';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { GroupBy } from '@ghostfolio/common/types';
import { DateRange, GroupBy } from '@ghostfolio/common/types';
import {
BarController,
BarElement,
@ -35,7 +36,15 @@ import {
Tooltip
} from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import { addDays, isAfter, parseISO, subDays } from 'date-fns';
import {
addDays,
format,
isAfter,
isBefore,
parseISO,
subDays
} from 'date-fns';
import { LineChartItem } from '@ghostfolio/common/interfaces';
@Component({
selector: 'gf-investment-chart',
@ -44,17 +53,19 @@ import { addDays, isAfter, parseISO, subDays } from 'date-fns';
styleUrls: ['./investment-chart.component.scss']
})
export class InvestmentChartComponent implements OnChanges, OnDestroy {
@Input() benchmarkDataItems: LineChartItem[] = [];
@Input() currency: string;
@Input() daysInMarket: number;
@Input() groupBy: GroupBy;
@Input() investments: InvestmentItem[];
@Input() isInPercent = false;
@Input() locale: string;
@Input() range: DateRange = 'max';
@Input() savingsRate = 0;
@ViewChild('chartCanvas') chartCanvas;
public chart: Chart;
public chart: Chart<any>;
public isLoading = true;
private data: InvestmentItem[];
@ -77,7 +88,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
}
public ngOnChanges() {
if (this.investments) {
if (this.benchmarkDataItems && this.investments) {
this.initialize();
}
}
@ -93,41 +104,61 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
this.data = this.investments.map((a) => Object.assign({}, a));
if (!this.groupBy && this.data?.length > 0) {
// Extend chart by 5% of days in market (before)
const firstItem = this.data[0];
this.data.unshift({
...firstItem,
date: subDays(
parseISO(firstItem.date),
this.daysInMarket * 0.05 || 90
).toISOString(),
investment: 0
});
if (this.range === 'max') {
// Extend chart by 5% of days in market (before)
const firstItem = this.data[0];
this.data.unshift({
...firstItem,
date: format(
subDays(parseISO(firstItem.date), this.daysInMarket * 0.05 || 90),
DATE_FORMAT
),
investment: 0
});
}
// Extend chart by 5% of days in market (after)
const lastItem = this.data[this.data.length - 1];
this.data.push({
...lastItem,
date: addDays(
parseDate(lastItem.date),
this.daysInMarket * 0.05 || 90
).toISOString()
date: format(
addDays(parseDate(lastItem.date), this.daysInMarket * 0.05 || 90),
DATE_FORMAT
)
});
}
let currentIndex = 0;
const totalAmountDataItems: { x: Date; y: number }[] = [];
for (const { date, value } of this.benchmarkDataItems) {
// TODO: Improve total amount calculation
if (
isBefore(parseDate(this.data?.[currentIndex]?.date), parseDate(date))
) {
currentIndex += 1;
}
totalAmountDataItems.push({
x: parseDate(date),
y: this.data?.[currentIndex]?.investment + value
});
}
const data = {
labels: this.data.map((investmentItem) => {
return investmentItem.date;
labels: this.benchmarkDataItems.map(({ date }) => {
return date;
}),
datasets: [
{
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderWidth: this.groupBy ? 0 : 2,
data: this.data.map((position) => {
return this.isInPercent
? position.investment * 100
: position.investment;
data: this.data.map(({ date, investment }) => {
return {
x: parseDate(date),
y: this.isInPercent ? investment * 100 : investment
};
}),
label: $localize`Deposit`,
segment: {
@ -139,6 +170,14 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
borderDash: (context: unknown) => this.isInFuture(context, [2, 2])
},
stepped: true
},
{
borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
borderWidth: 1,
data: totalAmountDataItems,
fill: false,
label: $localize`Total Amount`,
pointRadius: 0
}
]
};

27
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -39,6 +39,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
{ label: $localize`Accumulating`, value: undefined }
];
public performanceDataItems: HistoricalDataItem[];
public performanceDataItemsInPercentage: HistoricalDataItem[];
public top3: Position[];
public user: User;
@ -131,7 +132,24 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ chart }) => {
this.firstOrderDate = new Date(chart?.[0]?.date ?? new Date());
this.performanceDataItems = chart;
this.performanceDataItems = [];
this.performanceDataItemsInPercentage = [];
for (const {
date,
netPerformance,
netPerformanceInPercentage
} of chart) {
this.performanceDataItems.push({
date,
value: netPerformance
});
this.performanceDataItemsInPercentage.push({
date,
value: netPerformanceInPercentage
});
}
this.updateBenchmarkDataItems();
@ -139,7 +157,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
});
this.dataService
.fetchInvestments()
.fetchInvestments({ range: this.user?.settings?.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ firstOrderDate, investments }) => {
this.daysInMarket = differenceInDays(new Date(), firstOrderDate);
@ -149,7 +167,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
});
this.dataService
.fetchInvestmentsByMonth()
.fetchInvestments({
groupBy: 'month',
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ investments }) => {
this.investmentsByMonth = investments;

5
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -10,7 +10,7 @@
[daysInMarket]="daysInMarket"
[isLoading]="isLoadingBenchmarkComparator"
[locale]="user?.settings?.locale"
[performanceDataItems]="performanceDataItems"
[performanceDataItems]="performanceDataItemsInPercentage"
[user]="user"
(benchmarkChanged)="onChangeBenchmark($event)"
(dateRangeChanged)="onChangeDateRange($event)"
@ -119,12 +119,14 @@
<div class="chart-container">
<gf-investment-chart
class="h-100"
[benchmarkDataItems]="performanceDataItems"
[currency]="user?.settings?.baseCurrency"
[daysInMarket]="daysInMarket"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[investments]="investments"
[locale]="user?.settings?.locale"
[ngClass]="{ 'd-none': mode }"
[range]="user?.settings?.dateRange"
></gf-investment-chart>
<gf-investment-chart
class="h-100"
@ -135,6 +137,7 @@
[investments]="investmentsByMonth"
[locale]="user?.settings?.locale"
[ngClass]="{ 'd-none': !mode }"
[range]="user?.settings?.dateRange"
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate"
></gf-investment-chart>
</div>

24
apps/client/src/app/services/data.service.ts

@ -163,23 +163,15 @@ export class DataService {
return info;
}
public fetchInvestments(): Observable<PortfolioInvestments> {
return this.http.get<any>('/api/v1/portfolio/investments').pipe(
map((response) => {
if (response.firstOrderDate) {
response.firstOrderDate = parseISO(response.firstOrderDate);
}
return response;
})
);
}
public fetchInvestmentsByMonth(): Observable<PortfolioInvestments> {
public fetchInvestments({
groupBy,
range
}: {
groupBy?: 'month';
range: DateRange;
}): Observable<PortfolioInvestments> {
return this.http
.get<any>('/api/v1/portfolio/investments', {
params: { groupBy: 'month' }
})
.get<any>('/api/v1/portfolio/investments', { params: { groupBy, range } })
.pipe(
map((response) => {
if (response.firstOrderDate) {

2
libs/common/src/lib/interfaces/historical-data-item.interface.ts

@ -4,5 +4,5 @@ export interface HistoricalDataItem {
grossPerformancePercent?: number;
netPerformance?: number;
netPerformanceInPercentage?: number;
value: number;
value?: number;
}

Loading…
Cancel
Save