diff --git a/CHANGELOG.md b/CHANGELOG.md index 11436f14..46be289e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the portfolio analysis page: show the y-axis and extend the chart in relation to the days in market - Restructured the about page - Start refactoring _transactions_ to _activities_ - Refactored the demo user id diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index ce178ee1..44f4f1e7 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -10,12 +10,12 @@ import { baseCurrency } from '@ghostfolio/common/config'; import { PortfolioChart, PortfolioDetails, + PortfolioInvestments, PortfolioPerformance, PortfolioPublicDetails, PortfolioReport, PortfolioSummary } from '@ghostfolio/common/interfaces'; -import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import type { RequestWithUser } from '@ghostfolio/common/types'; import { Controller, @@ -48,42 +48,6 @@ export class PortfolioController { private readonly userService: UserService ) {} - @Get('investments') - @UseGuards(AuthGuard('jwt')) - public async findAll( - @Headers('impersonation-id') impersonationId: string, - @Res() res: Response - ): Promise { - if ( - this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && - this.request.user.subscription.type === 'Basic' - ) { - res.status(StatusCodes.FORBIDDEN); - return res.json([]); - } - - let investments = await this.portfolioService.getInvestments( - impersonationId - ); - - if ( - impersonationId || - this.userService.isRestrictedView(this.request.user) - ) { - const maxInvestment = investments.reduce( - (investment, item) => Math.max(investment, item.investment), - 1 - ); - - investments = investments.map((item) => ({ - date: item.date, - investment: item.investment / maxInvestment - })); - } - - return res.json(investments); - } - @Get('chart') @UseGuards(AuthGuard('jwt')) public async getChart( @@ -200,6 +164,42 @@ export class PortfolioController { return res.json({ accounts, hasError, holdings }); } + @Get('investments') + @UseGuards(AuthGuard('jwt')) + public async getInvestments( + @Headers('impersonation-id') impersonationId: string, + @Res() res: Response + ): Promise { + if ( + this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && + this.request.user.subscription.type === 'Basic' + ) { + res.status(StatusCodes.FORBIDDEN); + return res.json({}); + } + + let investments = await this.portfolioService.getInvestments( + impersonationId + ); + + if ( + impersonationId || + this.userService.isRestrictedView(this.request.user) + ) { + const maxInvestment = investments.reduce( + (investment, item) => Math.max(investment, item.investment), + 1 + ); + + investments = investments.map((item) => ({ + date: item.date, + investment: item.investment / maxInvestment + })); + } + + return res.json({ firstOrderDate: investments[0]?.date, investments }); + } + @Get('performance') @UseGuards(AuthGuard('jwt')) public async getPerformance( diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 81590027..1902fd13 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -55,7 +55,7 @@ import { subDays, subYears } from 'date-fns'; -import { isEmpty } from 'lodash'; +import { isEmpty, sortBy } from 'lodash'; import { HistoricalDataContainer, @@ -150,12 +150,33 @@ export class PortfolioService { return []; } - return portfolioCalculator.getInvestments().map((item) => { + const investments = portfolioCalculator.getInvestments().map((item) => { return { date: item.date, investment: item.investment.toNumber() }; }); + + // 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) => { + return investment.date; + }); } public async getChart( diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index a8b99ea4..d21d0d9c 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -10,6 +10,7 @@ import { ViewChild } from '@angular/core'; import { primaryColorRgb } from '@ghostfolio/common/config'; +import { parseDate } from '@ghostfolio/common/helper'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { Chart, @@ -19,7 +20,7 @@ import { PointElement, TimeScale } from 'chart.js'; -import { addMonths, isAfter, parseISO, subMonths } from 'date-fns'; +import { addDays, isAfter, parseISO, subDays } from 'date-fns'; @Component({ selector: 'gf-investment-chart', @@ -27,8 +28,10 @@ import { addMonths, isAfter, parseISO, subMonths } from 'date-fns'; templateUrl: './investment-chart.component.html', styleUrls: ['./investment-chart.component.scss'] }) -export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit { +export class InvestmentChartComponent implements OnChanges, OnDestroy { + @Input() daysInMarket: number; @Input() investments: InvestmentItem[]; + @Input() isInPercent = false; @ViewChild('chartCanvas') chartCanvas; @@ -45,8 +48,6 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit { ); } - public ngOnInit() {} - public ngOnChanges() { if (this.investments) { this.initialize(); @@ -61,19 +62,25 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit { this.isLoading = true; if (this.investments?.length > 0) { - // Extend chart by three months (before) + // Extend chart by 5% of days in market (before) const firstItem = this.investments[0]; this.investments.unshift({ ...firstItem, - date: subMonths(parseISO(firstItem.date), 3).toISOString(), + date: subDays( + parseISO(firstItem.date), + this.daysInMarket * 0.05 || 90 + ).toISOString(), investment: 0 }); - // Extend chart by three months (after) + // Extend chart by 5% of days in market (after) const lastItem = this.investments[this.investments.length - 1]; this.investments.push({ ...lastItem, - date: addMonths(new Date(), 3).toISOString() + date: addDays( + parseDate(lastItem.date), + this.daysInMarket * 0.05 || 90 + ).toISOString() }); } @@ -136,12 +143,26 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit { } }, y: { - display: false, + display: !this.isInPercent, grid: { display: false }, ticks: { - display: false + display: true, + callback: (tickValue, index, ticks) => { + if (index === 0 || index === ticks.length - 1) { + // Only print last and first legend entry + if (typeof tickValue === 'number') { + return tickValue.toFixed(2); + } + + return tickValue; + } + + return ''; + }, + mirror: true, + z: 1 } } } diff --git a/apps/client/src/app/components/investment-chart/investment-chart.module.ts b/apps/client/src/app/components/investment-chart/investment-chart.module.ts index faef8596..2af174b1 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.module.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.module.ts @@ -7,7 +7,6 @@ import { InvestmentChartComponent } from './investment-chart.component'; @NgModule({ declarations: [InvestmentChartComponent], exports: [InvestmentChartComponent], - imports: [CommonModule, NgxSkeletonLoaderModule], - providers: [] + imports: [CommonModule, NgxSkeletonLoaderModule] }) export class GfInvestmentChartModule {} diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index e2ed6065..1c9b872f 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -2,9 +2,9 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { PortfolioPosition, User } from '@ghostfolio/common/interfaces'; +import { 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 { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -16,28 +16,10 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './analysis-page.html' }) export class AnalysisPageComponent implements OnDestroy, OnInit { - public accounts: { - [symbol: string]: Pick & { value: number }; - }; - public continents: { - [code: string]: { name: string; value: number }; - }; - public countries: { - [code: string]: { name: string; value: number }; - }; + public daysInMarket: number; public deviceType: string; public hasImpersonationId: boolean; - public period = 'current'; - public periodOptions: ToggleOption[] = [ - { label: 'Initial', value: 'original' }, - { label: 'Current', value: 'current' } - ]; public investments: InvestmentItem[]; - public portfolioPositions: { [symbol: string]: PortfolioPosition }; - public positions: { [symbol: string]: any }; - public sectors: { - [name: string]: { name: string; value: number }; - }; public user: User; private unsubscribeSubject = new Subject(); @@ -69,8 +51,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { this.dataService .fetchInvestments() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((response) => { - this.investments = response; + .subscribe(({ firstOrderDate, investments }) => { + this.daysInMarket = differenceInDays(new Date(), firstOrderDate); + this.investments = investments; this.changeDetectorRef.markForCheck(); }); diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html index 66cda46c..4d396d6c 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html @@ -11,6 +11,8 @@ diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 6191fd9b..d975c4cb 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -5,7 +5,6 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; -import { PortfolioPositionDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface'; import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface'; @@ -23,13 +22,13 @@ import { InfoItem, PortfolioChart, PortfolioDetails, + PortfolioInvestments, PortfolioPerformance, PortfolioPublicDetails, PortfolioReport, PortfolioSummary, User } from '@ghostfolio/common/interfaces'; -import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { permissions } from '@ghostfolio/common/permissions'; import { DateRange } from '@ghostfolio/common/types'; import { DataSource, Order as OrderModel } from '@prisma/client'; @@ -124,6 +123,18 @@ export class DataService { return info; } + public fetchInvestments(): Observable { + return this.http.get('/api/portfolio/investments').pipe( + map((response) => { + if (response.firstOrderDate) { + response.firstOrderDate = parseISO(response.firstOrderDate); + } + + return response; + }) + ); + } + public fetchSymbolItem({ dataSource, includeHistoricalData = false, @@ -170,10 +181,6 @@ export class DataService { ); } - public fetchInvestments() { - return this.http.get('/api/portfolio/investments'); - } - public fetchPortfolioDetails(aParams: { [param: string]: any }) { return this.http.get('/api/portfolio/details', { params: aParams diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index d9bcc3a8..d88a572c 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -8,6 +8,7 @@ import { Export } from './export.interface'; import { InfoItem } from './info-item.interface'; import { PortfolioChart } from './portfolio-chart.interface'; import { PortfolioDetails } from './portfolio-details.interface'; +import { PortfolioInvestments } from './portfolio-investments.interface'; import { PortfolioItem } from './portfolio-item.interface'; import { PortfolioOverview } from './portfolio-overview.interface'; import { PortfolioPerformance } from './portfolio-performance.interface'; @@ -33,6 +34,7 @@ export { InfoItem, PortfolioChart, PortfolioDetails, + PortfolioInvestments, PortfolioItem, PortfolioOverview, PortfolioPerformance, diff --git a/libs/common/src/lib/interfaces/portfolio-investments.interface.ts b/libs/common/src/lib/interfaces/portfolio-investments.interface.ts new file mode 100644 index 00000000..06e91fbd --- /dev/null +++ b/libs/common/src/lib/interfaces/portfolio-investments.interface.ts @@ -0,0 +1,6 @@ +import { InvestmentItem } from './investment-item.interface'; + +export interface PortfolioInvestments { + firstOrderDate: Date; + investments: InvestmentItem[]; +}