diff --git a/CHANGELOG.md b/CHANGELOG.md index fc24feac..75552827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Combined the performance and chart calculation - Improved the style of various selectors (density) ## 1.196.0 - 22.09.2022 diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts index 9c60757f..68968b09 100644 --- a/apps/api/src/app/benchmark/benchmark.service.ts +++ b/apps/api/src/app/benchmark/benchmark.service.ts @@ -164,7 +164,7 @@ export class BenchmarkService { ); const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0; - return { + const response = { marketData: [ ...marketDataItems .filter((marketDataItem, index) => { @@ -181,17 +181,22 @@ export class BenchmarkService { marketDataItem.marketPrice ) * 100 }; - }), - { - date: format(new Date(), DATE_FORMAT), - value: - this.calculateChangeInPercentage( - marketPriceAtStartDate, - currentSymbolItem.marketPrice - ) * 100 - } + }) ] }; + + if (currentSymbolItem?.marketPrice) { + response.marketData.push({ + date: format(new Date(), DATE_FORMAT), + value: + this.calculateChangeInPercentage( + marketPriceAtStartDate, + currentSymbolItem.marketPrice + ) * 100 + }); + } + + return response; } private getMarketCondition(aPerformanceInPercent: number) { diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 44046a60..46fe092b 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -272,23 +272,20 @@ export class PortfolioCalculator { } } - const isInPercentage = true; - return Object.keys(totalNetPerformanceValues).map((date) => { - return isInPercentage - ? { - date, - value: totalInvestmentValues[date].eq(0) - ? 0 - : totalNetPerformanceValues[date] - .div(totalInvestmentValues[date]) - .mul(100) - .toNumber() - } - : { - date, - value: totalNetPerformanceValues[date].toNumber() - }; + const netPerformanceInPercentage = totalInvestmentValues[date].eq(0) + ? 0 + : totalNetPerformanceValues[date] + .div(totalInvestmentValues[date]) + .mul(100) + .toNumber(); + + return { + date, + netPerformanceInPercentage, + netPerformance: totalNetPerformanceValues[date].toNumber(), + value: netPerformanceInPercentage + }; }); } diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 86bc8c2f..66cd408c 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -110,26 +110,6 @@ export class PortfolioController { }; } - @Get('chart') - @UseGuards(AuthGuard('jwt')) - @Version('2') - public async getChartV2( - @Headers('impersonation-id') impersonationId: string, - @Query('range') range - ): Promise { - const historicalDataContainer = await this.portfolioService.getChartV2( - impersonationId, - range - ); - - return { - chart: historicalDataContainer.items, - hasError: false, - isAllTimeHigh: false, - isAllTimeLow: false - }; - } - @Get('details') @UseGuards(AuthGuard('jwt')) @UseInterceptors(RedactValuesInResponseInterceptor) @@ -319,6 +299,35 @@ export class PortfolioController { return performanceInformation; } + @Get('performance') + @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInResponseInterceptor) + @Version('2') + public async getPerformanceV2( + @Headers('impersonation-id') impersonationId: string, + @Query('range') dateRange + ): Promise { + const performanceInformation = await this.portfolioService.getPerformanceV2( + { + dateRange, + impersonationId + } + ); + + if ( + impersonationId || + this.request.user.Settings.settings.viewMode === 'ZEN' || + this.userService.isRestrictedView(this.request.user) + ) { + performanceInformation.performance = nullifyValuesInObject( + performanceInformation.performance, + ['currentGrossPerformance', 'currentNetPerformance', 'currentValue'] + ); + } + + return performanceInformation; + } + @Get('positions') @UseGuards(AuthGuard('jwt')) @UseInterceptors(TransformDataSourceInResponseInterceptor) diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 36b7bd48..c38e2a8d 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -355,11 +355,14 @@ export class PortfolioService { }; } - public async getChartV2( - aImpersonationId: string, - aDateRange: DateRange = 'max' - ): Promise { - const userId = await this.getUserId(aImpersonationId, this.request.user.id); + public async getChartV2({ + dateRange = 'max', + impersonationId + }: { + dateRange?: DateRange; + impersonationId: string; + }): Promise { + const userId = await this.getUserId(impersonationId, this.request.user.id); const { portfolioOrders, transactionPoints } = await this.getTransactionPoints({ @@ -383,7 +386,7 @@ export class PortfolioService { const endDate = new Date(); const portfolioStart = parseDate(transactionPoints[0].date); - const startDate = this.getStartDate(aDateRange, portfolioStart); + const startDate = this.getStartDate(dateRange, portfolioStart); const daysInMarket = differenceInDays(new Date(), startDate); const step = Math.round( @@ -987,6 +990,105 @@ export class PortfolioService { }; } + public async getPerformanceV2({ + dateRange = 'max', + impersonationId + }: { + dateRange?: DateRange; + impersonationId: string; + }): Promise { + const userId = await this.getUserId(impersonationId, 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 + }); + + if (transactionPoints?.length <= 0) { + return { + chart: [], + hasErrors: false, + performance: { + currentGrossPerformance: 0, + currentGrossPerformancePercent: 0, + currentNetPerformance: 0, + currentNetPerformancePercent: 0, + currentValue: 0 + } + }; + } + + portfolioCalculator.setTransactionPoints(transactionPoints); + + const portfolioStart = parseDate(transactionPoints[0].date); + const startDate = this.getStartDate(dateRange, portfolioStart); + const currentPositions = await portfolioCalculator.getCurrentPositions( + startDate + ); + + const hasErrors = currentPositions.hasErrors; + const currentValue = currentPositions.currentValue.toNumber(); + const currentGrossPerformance = currentPositions.grossPerformance; + const currentGrossPerformancePercent = + currentPositions.grossPerformancePercentage; + let currentNetPerformance = currentPositions.netPerformance; + let currentNetPerformancePercent = + currentPositions.netPerformancePercentage; + + // if (currentGrossPerformance.mul(currentGrossPerformancePercent).lt(0)) { + // // If algebraic sign is different, harmonize it + // currentGrossPerformancePercent = currentGrossPerformancePercent.mul(-1); + // } + + // if (currentNetPerformance.mul(currentNetPerformancePercent).lt(0)) { + // // If algebraic sign is different, harmonize it + // currentNetPerformancePercent = currentNetPerformancePercent.mul(-1); + // } + + const historicalDataContainer = await this.getChartV2({ + dateRange, + impersonationId + }); + + const itemOfToday = historicalDataContainer.items.find((item) => { + return item.date === format(new Date(), DATE_FORMAT); + }); + + if (itemOfToday) { + currentNetPerformance = new Big(itemOfToday.netPerformance); + currentNetPerformancePercent = new Big( + itemOfToday.netPerformanceInPercentage + ).div(100); + } + + return { + chart: historicalDataContainer.items.map( + ({ date, netPerformanceInPercentage }) => { + return { + date, + value: netPerformanceInPercentage + }; + } + ), + errors: currentPositions.errors, + hasErrors: currentPositions.hasErrors || hasErrors, + performance: { + currentValue, + currentGrossPerformance: currentGrossPerformance.toNumber(), + currentGrossPerformancePercent: + currentGrossPerformancePercent.toNumber(), + currentNetPerformance: currentNetPerformance.toNumber(), + currentNetPerformancePercent: currentNetPerformancePercent.toNumber() + } + }; + } + public async getReport(impersonationId: string): Promise { const currency = this.request.user.Settings.settings.baseCurrency; const userId = await this.getUserId(impersonationId, this.request.user.id); diff --git a/apps/client/src/app/components/home-overview/home-overview.component.ts b/apps/client/src/app/components/home-overview/home-overview.component.ts index c03110d7..9c35fe8c 100644 --- a/apps/client/src/app/components/home-overview/home-overview.component.ts +++ b/apps/client/src/app/components/home-overview/home-overview.component.ts @@ -76,8 +76,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { !this.hasImpersonationId && !this.user.settings.isRestrictedView && this.user.settings.viewMode !== 'ZEN'; - - this.update(); } public onChangeDateRange(dateRange: DateRange) { @@ -104,36 +102,51 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { } private update() { + this.historicalDataItems = null; this.isLoadingPerformance = true; this.dataService - .fetchChart({ + .fetchPortfolioPerformance({ range: this.user?.settings?.dateRange, version: this.user?.settings?.isExperimentalFeatures ? 2 : 1 }) .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((chartData) => { - this.historicalDataItems = chartData.chart.map((chartDataItem) => { - return { - date: chartDataItem.date, - value: chartDataItem.value - }; - }); - this.isAllTimeHigh = chartData.isAllTimeHigh; - this.isAllTimeLow = chartData.isAllTimeLow; - - this.changeDetectorRef.markForCheck(); - }); - - this.dataService - .fetchPortfolioPerformance({ range: this.user?.settings?.dateRange }) - .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((response) => { this.errors = response.errors; this.hasError = response.hasErrors; this.performance = response.performance; this.isLoadingPerformance = false; + if (this.user?.settings?.isExperimentalFeatures) { + this.historicalDataItems = response.chart.map(({ date, value }) => { + return { + date, + value + }; + }); + } else { + this.dataService + .fetchChart({ + range: this.user?.settings?.dateRange, + version: 1 + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((chartData) => { + this.historicalDataItems = chartData.chart.map( + ({ date, value }) => { + return { + date, + value + }; + } + ); + this.isAllTimeHigh = chartData.isAllTimeHigh; + this.isAllTimeLow = chartData.isAllTimeLow; + + this.changeDetectorRef.markForCheck(); + }); + } + this.changeDetectorRef.markForCheck(); }); 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 775b3298..69bc9dc8 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 @@ -126,7 +126,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { this.isLoadingBenchmarkComparator = true; this.dataService - .fetchChart({ range: this.user?.settings?.dateRange, version: 2 }) + .fetchPortfolioPerformance({ + range: this.user?.settings?.dateRange, + version: 2 + }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ chart }) => { this.firstOrderDate = new Date(chart?.[0]?.date ?? new Date()); diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 49d7b176..d33faeb7 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -353,12 +353,16 @@ export class DataService { }); } - public fetchPortfolioPerformance(params: { [param: string]: any }) { + public fetchPortfolioPerformance({ + range, + version + }: { + range: DateRange; + version: number; + }) { return this.http.get( - '/api/v1/portfolio/performance', - { - params - } + `/api/v${version}/portfolio/performance`, + { params: { range } } ); } diff --git a/libs/common/src/lib/interfaces/historical-data-item.interface.ts b/libs/common/src/lib/interfaces/historical-data-item.interface.ts index 3bb98fdb..32495783 100644 --- a/libs/common/src/lib/interfaces/historical-data-item.interface.ts +++ b/libs/common/src/lib/interfaces/historical-data-item.interface.ts @@ -2,5 +2,7 @@ export interface HistoricalDataItem { averagePrice?: number; date: string; grossPerformancePercent?: number; + netPerformance?: number; + netPerformanceInPercentage?: number; value: number; } diff --git a/libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts index 3db6d3af..74e7801c 100644 --- a/libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/portfolio-performance-response.interface.ts @@ -1,6 +1,8 @@ +import { HistoricalDataItem } from '../historical-data-item.interface'; import { PortfolioPerformance } from '../portfolio-performance.interface'; import { ResponseError } from './errors.interface'; export interface PortfolioPerformanceResponse extends ResponseError { + chart?: HistoricalDataItem[]; performance: PortfolioPerformance; } diff --git a/libs/ui/src/lib/line-chart/line-chart.component.ts b/libs/ui/src/lib/line-chart/line-chart.component.ts index 4f5d0571..7c0318d3 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -93,7 +93,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { } public ngOnChanges() { - if (this.historicalDataItems) { + if (this.historicalDataItems || this.historicalDataItems === null) { setTimeout(() => { // Wait for the chartCanvas this.initialize();