Feature/add group by year option on analysis page (#1568)

* Add group by year option
This commit is contained in:
Yash Solanki 2023-01-05 00:43:13 +05:30 committed by GitHub
parent 158bb00b8a
commit 925d38703e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 162 additions and 89 deletions

View File

@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### 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 Français (`fr`)
- Set up the language localization for Português (`pt`) - Set up the language localization for Português (`pt`)

View File

@ -64,7 +64,8 @@ describe('PortfolioCalculator', () => {
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth(); const investmentsByMonth =
portfolioCalculator.getInvestmentsByGroup('month');
spy.mockRestore(); spy.mockRestore();

View File

@ -53,7 +53,8 @@ describe('PortfolioCalculator', () => {
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth(); const investmentsByMonth =
portfolioCalculator.getInvestmentsByGroup('month');
spy.mockRestore(); spy.mockRestore();

View File

@ -64,7 +64,8 @@ describe('PortfolioCalculator', () => {
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth(); const investmentsByMonth =
portfolioCalculator.getInvestmentsByGroup('month');
spy.mockRestore(); spy.mockRestore();

View File

@ -41,7 +41,8 @@ describe('PortfolioCalculator', () => {
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth(); const investmentsByMonth =
portfolioCalculator.getInvestmentsByGroup('month');
spy.mockRestore(); spy.mockRestore();

View File

@ -64,7 +64,8 @@ describe('PortfolioCalculator', () => {
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth(); const investmentsByMonth =
portfolioCalculator.getInvestmentsByGroup('month');
spy.mockRestore(); spy.mockRestore();

View File

@ -68,7 +68,8 @@ describe('PortfolioCalculator', () => {
const investments = portfolioCalculator.getInvestments(); const investments = portfolioCalculator.getInvestments();
const investmentsByMonth = portfolioCalculator.getInvestmentsByMonth(); const investmentsByMonth =
portfolioCalculator.getInvestmentsByGroup('month');
spy.mockRestore(); spy.mockRestore();

View File

@ -2,6 +2,7 @@ import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/
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';
import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces'; import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces';
import { GroupBy } from '@ghostfolio/common/types';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { Type as TypeOfOrder } from '@prisma/client'; import { Type as TypeOfOrder } from '@prisma/client';
import Big from 'big.js'; 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) { if (this.orders.length === 0) {
return []; return [];
} }
const investments = []; const investments = [];
let currentDate: Date; let currentDate: Date;
let investmentByMonth = new Big(0); let investmentByGroup = new Big(0);
for (const [index, order] of this.orders.entries()) { for (const [index, order] of this.orders.entries()) {
if ( 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)) order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
); );
} else { } else {
// New month: Store previous month and reset // New group: Store previous group and reset
if (currentDate) { if (currentDate) {
investments.push({ investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT), date: format(
investment: investmentByMonth set(currentDate, {
date: 1,
month: groupBy === 'year' ? 0 : currentDate.getMonth()
}),
DATE_FORMAT
),
investment: investmentByGroup
}); });
} }
currentDate = parseDate(order.date); currentDate = parseDate(order.date);
investmentByMonth = order.quantity investmentByGroup = order.quantity
.mul(order.unitPrice) .mul(order.unitPrice)
.mul(this.getFactor(order.type)); .mul(this.getFactor(order.type));
} }
if (index === this.orders.length - 1) { if (index === this.orders.length - 1) {
// Store current month (latest order) // Store current group (latest order)
investments.push({ investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT), date: format(
investment: investmentByMonth set(currentDate, {
date: 1,
month: groupBy === 'year' ? 0 : currentDate.getMonth()
}),
DATE_FORMAT
),
investment: investmentByGroup
}); });
} }
} }

View File

@ -235,8 +235,8 @@ export class PortfolioService {
}; };
}); });
if (groupBy === 'month') { if (groupBy) {
dividends = this.getDividendsByMonth(dividends); dividends = this.getDividendsByGroup({ dividends, groupBy });
} }
const startDate = this.getStartDate( const startDate = this.getStartDate(
@ -282,26 +282,31 @@ export class PortfolioService {
let investments: InvestmentItem[]; let investments: InvestmentItem[];
if (groupBy === 'month') { if (groupBy) {
investments = portfolioCalculator.getInvestmentsByMonth().map((item) => { investments = portfolioCalculator
return { .getInvestmentsByGroup(groupBy)
date: item.date, .map((item) => {
investment: item.investment.toNumber() return {
}; date: item.date,
}); investment: item.investment.toNumber()
};
});
// Add investment of current month // Add investment of current group
const dateOfCurrentMonth = format( const dateOfCurrentGroup = format(
set(new Date(), { date: 1 }), set(new Date(), {
date: 1,
month: groupBy === 'year' ? 0 : new Date().getMonth()
}),
DATE_FORMAT DATE_FORMAT
); );
const investmentOfCurrentMonth = investments.filter(({ date }) => { const investmentOfCurrentGroup = investments.filter(({ date }) => {
return date === dateOfCurrentMonth; return date === dateOfCurrentGroup;
}); });
if (investmentOfCurrentMonth.length <= 0) { if (investmentOfCurrentGroup.length <= 0) {
investments.push({ investments.push({
date: dateOfCurrentMonth, date: dateOfCurrentGroup,
investment: 0 investment: 0
}); });
} }
@ -1264,47 +1269,66 @@ export class PortfolioService {
); );
} }
private getDividendsByMonth(aDividends: InvestmentItem[]): InvestmentItem[] { private getDividendsByGroup({
if (aDividends.length === 0) { dividends,
groupBy
}: {
dividends: InvestmentItem[];
groupBy: GroupBy;
}): InvestmentItem[] {
if (dividends.length === 0) {
return []; return [];
} }
const dividends = []; const dividendsByGroup: InvestmentItem[] = [];
let currentDate: Date; 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 ( 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 { } else {
// New month: Store previous month and reset // New group: Store previous group and reset
if (currentDate) { if (currentDate) {
dividends.push({ dividendsByGroup.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT), date: format(
investment: investmentByMonth set(currentDate, {
date: 1,
month: groupBy === 'year' ? 0 : currentDate.getMonth()
}),
DATE_FORMAT
),
investment: investmentByGroup.toNumber()
}); });
} }
currentDate = parseDate(dividend.date); 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) // Store current month (latest order)
dividends.push({ dividendsByGroup.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT), date: format(
investment: investmentByMonth set(currentDate, {
date: 1,
month: groupBy === 'year' ? 0 : currentDate.getMonth()
}),
DATE_FORMAT
),
investment: investmentByGroup.toNumber()
}); });
} }
} }
return dividends; return dividendsByGroup;
} }
private getFees({ private getFees({

View File

@ -198,6 +198,15 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
this.chart.options.scales.x.min = this.daysInMarket this.chart.options.scales.x.min = this.daysInMarket
? subDays(new Date(), this.daysInMarket).toISOString() ? subDays(new Date(), this.daysInMarket).toISOString()
: undefined; : undefined;
if (
this.savingsRate &&
this.chart.options.plugins.annotation.annotations.savingsRate
) {
this.chart.options.plugins.annotation.annotations.savingsRate.value =
this.savingsRate;
}
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart(this.chartCanvas.nativeElement, { this.chart = new Chart(this.chartCanvas.nativeElement, {

View File

@ -39,19 +39,20 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS; public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
public daysInMarket: number; public daysInMarket: number;
public deviceType: string; public deviceType: string;
public dividendsByMonth: InvestmentItem[]; public dividendsByGroup: InvestmentItem[];
public dividendTimelineDataLabel = $localize`Dividend`; public dividendTimelineDataLabel = $localize`Dividend`;
public filters$ = new Subject<Filter[]>(); public filters$ = new Subject<Filter[]>();
public firstOrderDate: Date; public firstOrderDate: Date;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public investments: InvestmentItem[]; public investments: InvestmentItem[];
public investmentTimelineDataLabel = $localize`Deposit`; public investmentTimelineDataLabel = $localize`Deposit`;
public investmentsByMonth: InvestmentItem[]; public investmentsByGroup: InvestmentItem[];
public isLoadingBenchmarkComparator: boolean; public isLoadingBenchmarkComparator: boolean;
public isLoadingInvestmentChart: boolean; public isLoadingInvestmentChart: boolean;
public mode: GroupBy = 'month'; public mode: GroupBy = 'month';
public modeOptions: ToggleOption[] = [ public modeOptions: ToggleOption[] = [
{ label: $localize`Monthly`, value: 'month' } { label: $localize`Monthly`, value: 'month' },
{ label: $localize`Yearly`, value: 'year' }
]; ];
public performanceDataItems: HistoricalDataItem[]; public performanceDataItems: HistoricalDataItem[];
public performanceDataItemsInPercentage: 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() { public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
@ -201,6 +213,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public onChangeGroupBy(aMode: GroupBy) { public onChangeGroupBy(aMode: GroupBy) {
this.mode = aMode; this.mode = aMode;
this.fetchDividendsAndInvestments();
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -208,6 +221,34 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete(); 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({ private openPositionDialog({
dataSource, dataSource,
symbol symbol
@ -291,32 +332,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); 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 this.dataService
.fetchPositions({ .fetchPositions({
filters: this.activeFilters, filters: this.activeFilters,
@ -340,6 +355,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
this.fetchDividendsAndInvestments();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }

View File

@ -180,15 +180,15 @@
<div class="chart-container"> <div class="chart-container">
<gf-investment-chart <gf-investment-chart
class="h-100" class="h-100"
groupBy="month" [benchmarkDataItems]="investmentsByGroup"
[benchmarkDataItems]="investmentsByMonth"
[benchmarkDataLabel]="investmentTimelineDataLabel" [benchmarkDataLabel]="investmentTimelineDataLabel"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[daysInMarket]="daysInMarket" [daysInMarket]="daysInMarket"
[groupBy]="mode"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[range]="user?.settings?.dateRange" [range]="user?.settings?.dateRange"
[savingsRate]="(hasImpersonationId || user.settings.isRestrictedView) ? undefined : user?.settings?.savingsRate" [savingsRate]="savingsRate"
></gf-investment-chart> ></gf-investment-chart>
</div> </div>
</div> </div>
@ -217,11 +217,11 @@
<div class="chart-container"> <div class="chart-container">
<gf-investment-chart <gf-investment-chart
class="h-100" class="h-100"
groupBy="month" [benchmarkDataItems]="dividendsByGroup"
[benchmarkDataItems]="dividendsByMonth"
[benchmarkDataLabel]="dividendTimelineDataLabel" [benchmarkDataLabel]="dividendTimelineDataLabel"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[daysInMarket]="daysInMarket" [daysInMarket]="daysInMarket"
[groupBy]="mode"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[range]="user?.settings?.dateRange" [range]="user?.settings?.dateRange"

View File

@ -10,5 +10,6 @@ export interface UserSettings {
isRestrictedView?: boolean; isRestrictedView?: boolean;
language?: string; language?: string;
locale?: string; locale?: string;
savingsRate?: number;
viewMode?: ViewMode; viewMode?: ViewMode;
} }

View File

@ -1 +1 @@
export type GroupBy = 'month'; export type GroupBy = 'month' | 'year';