From 7667af059c92aed569534267e4eb89c9bfb45e8d Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 24 Sep 2022 13:12:40 +0200
Subject: [PATCH] Feature/combine performance and chart calculation (#1285)

* Combine performance and chart calculation endpoints

* Update changelog
---
 CHANGELOG.md                                  |   1 +
 .../src/app/benchmark/benchmark.service.ts    |  25 ++--
 .../src/app/portfolio/portfolio-calculator.ts |  29 ++---
 .../src/app/portfolio/portfolio.controller.ts |  49 +++++---
 .../src/app/portfolio/portfolio.service.ts    | 114 +++++++++++++++++-
 .../home-overview/home-overview.component.ts  |  51 +++++---
 .../analysis/analysis-page.component.ts       |   5 +-
 apps/client/src/app/services/data.service.ts  |  14 ++-
 .../historical-data-item.interface.ts         |   2 +
 ...ortfolio-performance-response.interface.ts |   2 +
 .../lib/line-chart/line-chart.component.ts    |   2 +-
 11 files changed, 216 insertions(+), 78 deletions(-)

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<PortfolioChart> {
-    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<PortfolioPerformanceResponse> {
+    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<HistoricalDataContainer> {
-    const userId = await this.getUserId(aImpersonationId, this.request.user.id);
+  public async getChartV2({
+    dateRange = 'max',
+    impersonationId
+  }: {
+    dateRange?: DateRange;
+    impersonationId: string;
+  }): Promise<HistoricalDataContainer> {
+    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<PortfolioPerformanceResponse> {
+    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<PortfolioReport> {
     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<PortfolioPerformanceResponse>(
-      '/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();