Browse Source

Extend investment timeline by month

pull/1066/head
Thomas 3 years ago
parent
commit
0289597abf
  1. 45
      apps/api/src/app/portfolio/portfolio-calculator.ts
  2. 17
      apps/api/src/app/portfolio/portfolio.controller.ts
  3. 50
      apps/api/src/app/portfolio/portfolio.service.ts
  4. 18
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  5. 20
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  6. 26
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  7. 2
      apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts
  8. 16
      apps/client/src/app/services/data.service.ts
  9. 2
      libs/common/src/lib/chart-helper.ts

45
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -14,8 +14,11 @@ import {
format,
isAfter,
isBefore,
isSameMonth,
isSameYear,
max,
min
min,
set
} from 'date-fns';
import { first, flatten, isNumber, sortBy } from 'lodash';
@ -323,6 +326,46 @@ export class PortfolioCalculator {
});
}
public getInvestmentsByMonth(): { date: string; investment: Big }[] {
if (this.orders.length === 0) {
return [];
}
const investments = [];
let currentDate = parseDate(this.orders[0].date);
let investmentByMonth = new Big(0);
for (const [index, order] of this.orders.entries()) {
if (
isSameMonth(parseDate(order.date), currentDate) &&
isSameYear(parseDate(order.date), currentDate)
) {
investmentByMonth = investmentByMonth.plus(
order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
);
if (index === this.orders.length - 1) {
investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
investment: investmentByMonth
});
}
} else {
investments.push({
date: format(set(currentDate, { date: 1 }), DATE_FORMAT),
investment: investmentByMonth
});
currentDate = parseDate(order.date);
investmentByMonth = order.quantity
.mul(order.unitPrice)
.mul(this.getFactor(order.type));
}
}
return investments;
}
public async calculateTimeline(
timelineSpecification: TimelineSpecification[],
endDate: string

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

@ -20,6 +20,7 @@ import {
PortfolioReport,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
@ -217,7 +218,8 @@ export class PortfolioController {
@Get('investments')
@UseGuards(AuthGuard('jwt'))
public async getInvestments(
@Headers('impersonation-id') impersonationId: string
@Headers('impersonation-id') impersonationId: string,
@Query('groupBy') groupBy?: string // TODO: Add type
): Promise<PortfolioInvestments> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
@ -229,9 +231,16 @@ export class PortfolioController {
);
}
let investments = await this.portfolioService.getInvestments(
impersonationId
);
let investments: InvestmentItem[];
if (groupBy === 'month') {
investments = await this.portfolioService.getInvestments(
impersonationId,
'month'
);
} else {
investments = await this.portfolioService.getInvestments(impersonationId);
}
if (
impersonationId ||

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

@ -183,7 +183,8 @@ export class PortfolioService {
}
public async getInvestments(
aImpersonationId: string
aImpersonationId: string,
groupBy?: string
): Promise<InvestmentItem[]> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
@ -204,28 +205,39 @@ export class PortfolioService {
return [];
}
const investments = portfolioCalculator.getInvestments().map((item) => {
return {
date: item.date,
investment: item.investment.toNumber()
};
});
let investments: InvestmentItem[];
// Add investment of today
const investmentOfToday = investments.filter((investment) => {
return investment.date === format(new Date(), DATE_FORMAT);
});
if (investmentOfToday.length <= 0) {
const pastInvestments = investments.filter((investment) => {
return isBefore(parseDate(investment.date), new Date());
if (groupBy === 'month') {
investments = portfolioCalculator.getInvestmentsByMonth().map((item) => {
return {
date: item.date,
investment: item.investment.toNumber()
};
});
} else {
investments = portfolioCalculator.getInvestments().map((item) => {
return {
date: item.date,
investment: item.investment.toNumber()
};
});
const lastInvestment = pastInvestments[pastInvestments.length - 1];
investments.push({
date: format(new Date(), DATE_FORMAT),
investment: lastInvestment?.investment ?? 0
// Add investment of today
const investmentOfToday = investments.filter((investment) => {
return investment.date === format(new Date(), DATE_FORMAT);
});
if (investmentOfToday.length <= 0) {
const pastInvestments = investments.filter((investment) => {
return isBefore(parseDate(investment.date), new Date());
});
const lastInvestment = pastInvestments[pastInvestments.length - 1];
investments.push({
date: format(new Date(), DATE_FORMAT),
investment: lastInvestment?.investment ?? 0
});
}
}
return sortBy(investments, (investment) => {

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

@ -29,7 +29,9 @@ import {
LinearScale,
PointElement,
TimeScale,
Tooltip
Tooltip,
BarController,
BarElement
} from 'chart.js';
import { addDays, isAfter, parseISO, subDays } from 'date-fns';
@ -42,6 +44,7 @@ import { addDays, isAfter, parseISO, subDays } from 'date-fns';
export class InvestmentChartComponent implements OnChanges, OnDestroy {
@Input() currency: string;
@Input() daysInMarket: number;
@Input() groupBy: string;
@Input() investments: InvestmentItem[];
@Input() isInPercent = false;
@Input() locale: string;
@ -53,6 +56,8 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
public constructor() {
Chart.register(
BarController,
BarElement,
LinearScale,
LineController,
LineElement,
@ -78,7 +83,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
private initialize() {
this.isLoading = true;
if (this.investments?.length > 0) {
if (!this.groupBy && this.investments?.length > 0) {
// Extend chart by 5% of days in market (before)
const firstItem = this.investments[0];
this.investments.unshift({
@ -102,13 +107,14 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
}
const data = {
labels: this.investments.map((position) => {
return position.date;
labels: this.investments.map((investmentItem) => {
return investmentItem.date;
}),
datasets: [
{
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderWidth: 2,
borderWidth: this.groupBy ? 0 : 2,
data: this.investments.map((position) => {
return position.investment;
}),
@ -192,7 +198,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
}
},
plugins: [getVerticalHoverLinePlugin(this.chartCanvas)],
type: 'line'
type: this.groupBy ? 'bar' : 'line'
});
this.isLoading = false;

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

@ -4,6 +4,7 @@ import { ImpersonationStorageService } from '@ghostfolio/client/services/imperso
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { Position, User } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { ToggleOption } from '@ghostfolio/common/types';
import { differenceInDays } from 'date-fns';
import { sortBy } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector';
@ -22,6 +23,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public deviceType: string;
public hasImpersonationId: boolean;
public investments: InvestmentItem[];
public investmentsByMonth: InvestmentItem[];
public mode;
public modeOptions: ToggleOption[] = [
{ label: 'Monthly', value: 'month' },
{ label: 'Accumulating', value: undefined }
];
public top3: Position[];
public user: User;
@ -55,6 +62,15 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchInvestmentsByMonth()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ firstOrderDate, investments }) => {
this.investmentsByMonth = investments;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPositions({ range: 'max' })
.pipe(takeUntil(this.unsubscribeSubject))
@ -86,6 +102,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
});
}
public onChangeGroupBy(aValue: string) {
this.mode = aValue;
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();

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

@ -2,8 +2,19 @@
<div class="investment-chart row">
<div class="col-lg">
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
<div class="mb-3">
<div class="h5 mb-3" i18n>Investment Timeline</div>
<div class="mb-4">
<div class="align-items-center d-flex mb-4">
<div class="flex-grow-1 h5 mb-0 text-truncate" i18n>
Investment Timeline
</div>
<gf-toggle
class="d-none d-lg-block"
[defaultValue]="mode"
[isLoading]="false"
[options]="modeOptions"
(change)="onChangeGroupBy($event.value)"
></gf-toggle>
</div>
<gf-investment-chart
class="h-100"
[currency]="user?.settings?.baseCurrency"
@ -11,6 +22,17 @@
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[investments]="investments"
[locale]="user?.settings?.locale"
[ngClass]="{ 'd-none': mode }"
></gf-investment-chart>
<gf-investment-chart
class="h-100"
groupBy="month"
[currency]="user?.settings?.baseCurrency"
[daysInMarket]="daysInMarket"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[investments]="investmentsByMonth"
[locale]="user?.settings?.locale"
[ngClass]="{ 'd-none': !mode }"
></gf-investment-chart>
</div>
</div>

2
apps/client/src/app/pages/portfolio/analysis/analysis-page.module.ts

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -14,6 +15,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
AnalysisPageRoutingModule,
CommonModule,
GfInvestmentChartModule,
GfToggleModule,
GfValueModule,
MatCardModule,
NgxSkeletonLoaderModule

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

@ -204,6 +204,22 @@ export class DataService {
);
}
public fetchInvestmentsByMonth(): Observable<PortfolioInvestments> {
return this.http
.get<any>('/api/v1/portfolio/investments', {
params: { groupBy: 'month' }
})
.pipe(
map((response) => {
if (response.firstOrderDate) {
response.firstOrderDate = parseISO(response.firstOrderDate);
}
return response;
})
);
}
public fetchSymbolItem({
dataSource,
includeHistoricalData,

2
libs/common/src/lib/chart-helper.ts

@ -43,7 +43,7 @@ export function getTooltipPositionerMapTop(
chart: Chart,
position: TooltipPosition
) {
if (!position) {
if (!position || !chart?.chartArea) {
return false;
}
return {

Loading…
Cancel
Save