Feature/add group by year option on analysis page (#1568)
* Add group by year option
This commit is contained in:
parent
158bb00b8a
commit
925d38703e
@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for the dividend timeline grouped by year
|
||||
- Added support for the investment timeline grouped by year
|
||||
- Set up the language localization for Français (`fr`)
|
||||
- Set up the language localization for Português (`pt`)
|
||||
|
||||
|
@ -64,7 +64,8 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
|
@ -53,7 +53,8 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
|
@ -64,7 +64,8 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
|
@ -41,7 +41,8 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
|
@ -64,7 +64,8 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
|
@ -68,7 +68,8 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
const investments = portfolioCalculator.getInvestments();
|
||||
|
||||
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth();
|
||||
const investmentsByMonth =
|
||||
portfolioCalculator.getInvestmentsByGroup('month');
|
||||
|
||||
spy.mockRestore();
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/
|
||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||
import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
|
||||
import { GroupBy } from '@ghostfolio/common/types';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Type as TypeOfOrder } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
@ -478,46 +479,60 @@ export class PortfolioCalculator {
|
||||
});
|
||||
}
|
||||
|
||||
public getInvestmentsByMonth(): { date: string; investment: Big }[] {
|
||||
public getInvestmentsByGroup(
|
||||
groupBy: GroupBy
|
||||
): { date: string; investment: Big }[] {
|
||||
if (this.orders.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const investments = [];
|
||||
let currentDate: Date;
|
||||
let investmentByMonth = new Big(0);
|
||||
let investmentByGroup = new Big(0);
|
||||
|
||||
for (const [index, order] of this.orders.entries()) {
|
||||
if (
|
||||
isSameMonth(parseDate(order.date), currentDate) &&
|
||||
isSameYear(parseDate(order.date), currentDate)
|
||||
isSameYear(parseDate(order.date), currentDate) &&
|
||||
(groupBy === 'year' || isSameMonth(parseDate(order.date), currentDate))
|
||||
) {
|
||||
// Same month: Add up investments
|
||||
// Same group: Add up investments
|
||||
|
||||
investmentByMonth = investmentByMonth.plus(
|
||||
investmentByGroup = investmentByGroup.plus(
|
||||
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
|
||||
);
|
||||
} else {
|
||||
// New month: Store previous month and reset
|
||||
// New group: Store previous group and reset
|
||||
|
||||
if (currentDate) {
|
||||
investments.push({
|
||||
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
|
||||
investment: investmentByMonth
|
||||
date: format(
|
||||
set(currentDate, {
|
||||
date: 1,
|
||||
month: groupBy === 'year' ? 0 : currentDate.getMonth()
|
||||
}),
|
||||
DATE_FORMAT
|
||||
),
|
||||
investment: investmentByGroup
|
||||
});
|
||||
}
|
||||
|
||||
currentDate = parseDate(order.date);
|
||||
investmentByMonth = order.quantity
|
||||
investmentByGroup = order.quantity
|
||||
.mul(order.unitPrice)
|
||||
.mul(this.getFactor(order.type));
|
||||
}
|
||||
|
||||
if (index === this.orders.length - 1) {
|
||||
// Store current month (latest order)
|
||||
// Store current group (latest order)
|
||||
investments.push({
|
||||
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
|
||||
investment: investmentByMonth
|
||||
date: format(
|
||||
set(currentDate, {
|
||||
date: 1,
|
||||
month: groupBy === 'year' ? 0 : currentDate.getMonth()
|
||||
}),
|
||||
DATE_FORMAT
|
||||
),
|
||||
investment: investmentByGroup
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -235,8 +235,8 @@ export class PortfolioService {
|
||||
};
|
||||
});
|
||||
|
||||
if (groupBy === 'month') {
|
||||
dividends = this.getDividendsByMonth(dividends);
|
||||
if (groupBy) {
|
||||
dividends = this.getDividendsByGroup({ dividends, groupBy });
|
||||
}
|
||||
|
||||
const startDate = this.getStartDate(
|
||||
@ -282,26 +282,31 @@ export class PortfolioService {
|
||||
|
||||
let investments: InvestmentItem[];
|
||||
|
||||
if (groupBy === 'month') {
|
||||
investments = portfolioCalculator.getInvestmentsByMonth().map((item) => {
|
||||
if (groupBy) {
|
||||
investments = portfolioCalculator
|
||||
.getInvestmentsByGroup(groupBy)
|
||||
.map((item) => {
|
||||
return {
|
||||
date: item.date,
|
||||
investment: item.investment.toNumber()
|
||||
};
|
||||
});
|
||||
|
||||
// Add investment of current month
|
||||
const dateOfCurrentMonth = format(
|
||||
set(new Date(), { date: 1 }),
|
||||
// Add investment of current group
|
||||
const dateOfCurrentGroup = format(
|
||||
set(new Date(), {
|
||||
date: 1,
|
||||
month: groupBy === 'year' ? 0 : new Date().getMonth()
|
||||
}),
|
||||
DATE_FORMAT
|
||||
);
|
||||
const investmentOfCurrentMonth = investments.filter(({ date }) => {
|
||||
return date === dateOfCurrentMonth;
|
||||
const investmentOfCurrentGroup = investments.filter(({ date }) => {
|
||||
return date === dateOfCurrentGroup;
|
||||
});
|
||||
|
||||
if (investmentOfCurrentMonth.length <= 0) {
|
||||
if (investmentOfCurrentGroup.length <= 0) {
|
||||
investments.push({
|
||||
date: dateOfCurrentMonth,
|
||||
date: dateOfCurrentGroup,
|
||||
investment: 0
|
||||
});
|
||||
}
|
||||
@ -1264,47 +1269,66 @@ export class PortfolioService {
|
||||
);
|
||||
}
|
||||
|
||||
private getDividendsByMonth(aDividends: InvestmentItem[]): InvestmentItem[] {
|
||||
if (aDividends.length === 0) {
|
||||
private getDividendsByGroup({
|
||||
dividends,
|
||||
groupBy
|
||||
}: {
|
||||
dividends: InvestmentItem[];
|
||||
groupBy: GroupBy;
|
||||
}): InvestmentItem[] {
|
||||
if (dividends.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dividends = [];
|
||||
const dividendsByGroup: InvestmentItem[] = [];
|
||||
let currentDate: Date;
|
||||
let investmentByMonth = new Big(0);
|
||||
let investmentByGroup = new Big(0);
|
||||
|
||||
for (const [index, dividend] of aDividends.entries()) {
|
||||
for (const [index, dividend] of dividends.entries()) {
|
||||
if (
|
||||
isSameMonth(parseDate(dividend.date), currentDate) &&
|
||||
isSameYear(parseDate(dividend.date), currentDate)
|
||||
isSameYear(parseDate(dividend.date), currentDate) &&
|
||||
(groupBy === 'year' ||
|
||||
isSameMonth(parseDate(dividend.date), currentDate))
|
||||
) {
|
||||
// Same month: Add up divididends
|
||||
// Same group: Add up dividends
|
||||
|
||||
investmentByMonth = investmentByMonth.plus(dividend.investment);
|
||||
investmentByGroup = investmentByGroup.plus(dividend.investment);
|
||||
} else {
|
||||
// New month: Store previous month and reset
|
||||
// New group: Store previous group and reset
|
||||
|
||||
if (currentDate) {
|
||||
dividends.push({
|
||||
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
|
||||
investment: investmentByMonth
|
||||
dividendsByGroup.push({
|
||||
date: format(
|
||||
set(currentDate, {
|
||||
date: 1,
|
||||
month: groupBy === 'year' ? 0 : currentDate.getMonth()
|
||||
}),
|
||||
DATE_FORMAT
|
||||
),
|
||||
investment: investmentByGroup.toNumber()
|
||||
});
|
||||
}
|
||||
|
||||
currentDate = parseDate(dividend.date);
|
||||
investmentByMonth = new Big(dividend.investment);
|
||||
investmentByGroup = new Big(dividend.investment);
|
||||
}
|
||||
|
||||
if (index === aDividends.length - 1) {
|
||||
if (index === dividends.length - 1) {
|
||||
// Store current month (latest order)
|
||||
dividends.push({
|
||||
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
|
||||
investment: investmentByMonth
|
||||
dividendsByGroup.push({
|
||||
date: format(
|
||||
set(currentDate, {
|
||||
date: 1,
|
||||
month: groupBy === 'year' ? 0 : currentDate.getMonth()
|
||||
}),
|
||||
DATE_FORMAT
|
||||
),
|
||||
investment: investmentByGroup.toNumber()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return dividends;
|
||||
return dividendsByGroup;
|
||||
}
|
||||
|
||||
private getFees({
|
||||
|
@ -198,6 +198,15 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||
this.chart.options.scales.x.min = this.daysInMarket
|
||||
? subDays(new Date(), this.daysInMarket).toISOString()
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
this.savingsRate &&
|
||||
this.chart.options.plugins.annotation.annotations.savingsRate
|
||||
) {
|
||||
this.chart.options.plugins.annotation.annotations.savingsRate.value =
|
||||
this.savingsRate;
|
||||
}
|
||||
|
||||
this.chart.update();
|
||||
} else {
|
||||
this.chart = new Chart(this.chartCanvas.nativeElement, {
|
||||
|
@ -39,19 +39,20 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||
public daysInMarket: number;
|
||||
public deviceType: string;
|
||||
public dividendsByMonth: InvestmentItem[];
|
||||
public dividendsByGroup: InvestmentItem[];
|
||||
public dividendTimelineDataLabel = $localize`Dividend`;
|
||||
public filters$ = new Subject<Filter[]>();
|
||||
public firstOrderDate: Date;
|
||||
public hasImpersonationId: boolean;
|
||||
public investments: InvestmentItem[];
|
||||
public investmentTimelineDataLabel = $localize`Deposit`;
|
||||
public investmentsByMonth: InvestmentItem[];
|
||||
public investmentsByGroup: InvestmentItem[];
|
||||
public isLoadingBenchmarkComparator: boolean;
|
||||
public isLoadingInvestmentChart: boolean;
|
||||
public mode: GroupBy = 'month';
|
||||
public modeOptions: ToggleOption[] = [
|
||||
{ label: $localize`Monthly`, value: 'month' }
|
||||
{ label: $localize`Monthly`, value: 'month' },
|
||||
{ label: $localize`Yearly`, value: 'year' }
|
||||
];
|
||||
public performanceDataItems: HistoricalDataItem[];
|
||||
public performanceDataItemsInPercentage: HistoricalDataItem[];
|
||||
@ -91,6 +92,17 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
get savingsRate() {
|
||||
const savingsRatePerMonth =
|
||||
this.hasImpersonationId || this.user.settings.isRestrictedView
|
||||
? undefined
|
||||
: this.user?.settings?.savingsRate;
|
||||
|
||||
return this.mode === 'year'
|
||||
? savingsRatePerMonth * 12
|
||||
: savingsRatePerMonth;
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
@ -201,6 +213,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
public onChangeGroupBy(aMode: GroupBy) {
|
||||
this.mode = aMode;
|
||||
this.fetchDividendsAndInvestments();
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
@ -208,6 +221,34 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private fetchDividendsAndInvestments() {
|
||||
this.dataService
|
||||
.fetchDividends({
|
||||
filters: this.activeFilters,
|
||||
groupBy: this.mode,
|
||||
range: this.user?.settings?.dateRange
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ dividends }) => {
|
||||
this.dividendsByGroup = dividends;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchInvestments({
|
||||
filters: this.activeFilters,
|
||||
groupBy: this.mode,
|
||||
range: this.user?.settings?.dateRange
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ investments }) => {
|
||||
this.investmentsByGroup = investments;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
private openPositionDialog({
|
||||
dataSource,
|
||||
symbol
|
||||
@ -291,32 +332,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchDividends({
|
||||
filters: this.activeFilters,
|
||||
groupBy: 'month',
|
||||
range: this.user?.settings?.dateRange
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ dividends }) => {
|
||||
this.dividendsByMonth = dividends;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchInvestments({
|
||||
filters: this.activeFilters,
|
||||
groupBy: 'month',
|
||||
range: this.user?.settings?.dateRange
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ investments }) => {
|
||||
this.investmentsByMonth = investments;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchPositions({
|
||||
filters: this.activeFilters,
|
||||
@ -340,6 +355,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.fetchDividendsAndInvestments();
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
|
@ -180,15 +180,15 @@
|
||||
<div class="chart-container">
|
||||
<gf-investment-chart
|
||||
class="h-100"
|
||||
groupBy="month"
|
||||
[benchmarkDataItems]="investmentsByMonth"
|
||||
[benchmarkDataItems]="investmentsByGroup"
|
||||
[benchmarkDataLabel]="investmentTimelineDataLabel"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[groupBy]="mode"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[locale]="user?.settings?.locale"
|
||||
[range]="user?.settings?.dateRange"
|
||||
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate"
|
||||
[savingsRate]="savingsRate"
|
||||
></gf-investment-chart>
|
||||
</div>
|
||||
</div>
|
||||
@ -217,11 +217,11 @@
|
||||
<div class="chart-container">
|
||||
<gf-investment-chart
|
||||
class="h-100"
|
||||
groupBy="month"
|
||||
[benchmarkDataItems]="dividendsByMonth"
|
||||
[benchmarkDataItems]="dividendsByGroup"
|
||||
[benchmarkDataLabel]="dividendTimelineDataLabel"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[daysInMarket]="daysInMarket"
|
||||
[groupBy]="mode"
|
||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||
[locale]="user?.settings?.locale"
|
||||
[range]="user?.settings?.dateRange"
|
||||
|
@ -10,5 +10,6 @@ export interface UserSettings {
|
||||
isRestrictedView?: boolean;
|
||||
language?: string;
|
||||
locale?: string;
|
||||
savingsRate?: number;
|
||||
viewMode?: ViewMode;
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
export type GroupBy = 'month';
|
||||
export type GroupBy = 'month' | 'year';
|
||||
|
Loading…
x
Reference in New Issue
Block a user