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 07/29] 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 fc24feac1..755528274 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 9c60757f5..68968b09f 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 44046a60f..46fe092bb 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 86bc8c2fa..66cd408cc 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 36b7bd48e..c38e2a8d0 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 c03110d75..9c35fe8c5 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 775b32981..69bc9dc87 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 49d7b1761..d33faeb74 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 3bb98fdbe..324957838 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 3db6d3af4..74e7801cd 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 4f5d0571b..7c0318d3e 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();
From f3d337b044d142a042bcaf75f89f42541cf150ff Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 24 Sep 2022 13:15:16 +0200
Subject: [PATCH 08/29] Feature/add value of active filters on allocations page
(#1286)
* Add value
* Update changelog
---
CHANGELOG.md | 1 +
.../portfolio/allocations/allocations-page.html | 12 ++++++++++--
.../portfolio/allocations/allocations-page.scss | 1 +
3 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 755528274..c101591e3 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
### Added
+- Added the value of the active filter in percentage on the allocations page
- Extended the feature overview page by multi-language support (English, German, Italian)
### Changed
diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html
index 4a030c8a0..36e98011c 100644
--- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html
+++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html
@@ -13,8 +13,16 @@
-
- Proportion of Net Worth
+
+ Proportion of Net Worth
+
Date: Sat, 24 Sep 2022 13:16:47 +0200
Subject: [PATCH 09/29] Release 1.197.0 (#1287)
---
CHANGELOG.md | 2 +-
package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c101591e3..20277377b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## Unreleased
+## 1.197.0 - 24.09.2022
### Added
diff --git a/package.json b/package.json
index 5dde2d031..a11af30b0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ghostfolio",
- "version": "1.196.0",
+ "version": "1.197.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {
From 72974e888f3403ab4bc3e6f3bbb14a46a9fced94 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 25 Sep 2022 15:14:51 +0200
Subject: [PATCH 10/29] Clean up spaces (#1288)
---
.../20210605161257_added_symbol_profile/migration.sql | 2 +-
.../migrations/20210612110542_added_auth_device/migration.sql | 2 +-
.../migration.sql | 2 +-
.../20210703194509_added_balance_to_account/migration.sql | 4 ++--
.../migration.sql | 2 +-
.../20210807062952_added_is_draft_to_order/migration.sql | 2 +-
.../migration.sql | 2 +-
.../20210815180121_added_settings_to_settings/migration.sql | 2 +-
.../migration.sql | 2 +-
.../migration.sql | 2 +-
.../migration.sql | 2 +-
.../migration.sql | 2 +-
.../20220227093650_added_url_to_symbol_profile/migration.sql | 2 +-
13 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/prisma/migrations/20210605161257_added_symbol_profile/migration.sql b/prisma/migrations/20210605161257_added_symbol_profile/migration.sql
index 07c3f5ce0..b1fe9aa42 100644
--- a/prisma/migrations/20210605161257_added_symbol_profile/migration.sql
+++ b/prisma/migrations/20210605161257_added_symbol_profile/migration.sql
@@ -1,5 +1,5 @@
-- AlterTable
-ALTER TABLE "Order" ADD COLUMN "symbolProfileId" TEXT;
+ALTER TABLE "Order" ADD COLUMN "symbolProfileId" TEXT;
-- CreateTable
CREATE TABLE "SymbolProfile" (
diff --git a/prisma/migrations/20210612110542_added_auth_device/migration.sql b/prisma/migrations/20210612110542_added_auth_device/migration.sql
index 28d8d7c25..8d8675436 100644
--- a/prisma/migrations/20210612110542_added_auth_device/migration.sql
+++ b/prisma/migrations/20210612110542_added_auth_device/migration.sql
@@ -1,5 +1,5 @@
-- AlterTable
-ALTER TABLE "User" ADD COLUMN "authChallenge" TEXT;
+ALTER TABLE "User" ADD COLUMN "authChallenge" TEXT;
-- CreateTable
CREATE TABLE "AuthDevice" (
diff --git a/prisma/migrations/20210616075245_added_sectors_to_symbol_profile/migration.sql b/prisma/migrations/20210616075245_added_sectors_to_symbol_profile/migration.sql
index 39b7baba7..96621b085 100644
--- a/prisma/migrations/20210616075245_added_sectors_to_symbol_profile/migration.sql
+++ b/prisma/migrations/20210616075245_added_sectors_to_symbol_profile/migration.sql
@@ -1,2 +1,2 @@
-- AlterTable
-ALTER TABLE "SymbolProfile" ADD COLUMN "sectors" JSONB;
+ALTER TABLE "SymbolProfile" ADD COLUMN "sectors" JSONB;
diff --git a/prisma/migrations/20210703194509_added_balance_to_account/migration.sql b/prisma/migrations/20210703194509_added_balance_to_account/migration.sql
index 8c3952035..d9a9ffd1c 100644
--- a/prisma/migrations/20210703194509_added_balance_to_account/migration.sql
+++ b/prisma/migrations/20210703194509_added_balance_to_account/migration.sql
@@ -2,5 +2,5 @@
ALTER TYPE "AccountType" ADD VALUE 'CASH';
-- AlterTable
-ALTER TABLE "Account" ADD COLUMN "balance" DOUBLE PRECISION NOT NULL DEFAULT 0,
-ADD COLUMN "currency" "Currency" NOT NULL DEFAULT E'USD';
+ALTER TABLE "Account" ADD COLUMN "balance" DOUBLE PRECISION NOT NULL DEFAULT 0,
+ADD COLUMN "currency" "Currency" NOT NULL DEFAULT E'USD';
diff --git a/prisma/migrations/20210724160404_added_currency_to_symbol_profile/migration.sql b/prisma/migrations/20210724160404_added_currency_to_symbol_profile/migration.sql
index a99c3c1d9..eab96d9fc 100644
--- a/prisma/migrations/20210724160404_added_currency_to_symbol_profile/migration.sql
+++ b/prisma/migrations/20210724160404_added_currency_to_symbol_profile/migration.sql
@@ -1,2 +1,2 @@
-- AlterTable
-ALTER TABLE "SymbolProfile" ADD COLUMN "currency" "Currency";
+ALTER TABLE "SymbolProfile" ADD COLUMN "currency" "Currency";
diff --git a/prisma/migrations/20210807062952_added_is_draft_to_order/migration.sql b/prisma/migrations/20210807062952_added_is_draft_to_order/migration.sql
index 51970ec3c..d63f8679d 100644
--- a/prisma/migrations/20210807062952_added_is_draft_to_order/migration.sql
+++ b/prisma/migrations/20210807062952_added_is_draft_to_order/migration.sql
@@ -1,2 +1,2 @@
-- AlterTable
-ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;
+ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;
diff --git a/prisma/migrations/20210808075949_added_asset_class_to_symbol_profile/migration.sql b/prisma/migrations/20210808075949_added_asset_class_to_symbol_profile/migration.sql
index 5050a9033..da37ab8cb 100644
--- a/prisma/migrations/20210808075949_added_asset_class_to_symbol_profile/migration.sql
+++ b/prisma/migrations/20210808075949_added_asset_class_to_symbol_profile/migration.sql
@@ -2,4 +2,4 @@
CREATE TYPE "AssetClass" AS ENUM ('CASH', 'COMMODITY', 'EQUITY');
-- AlterTable
-ALTER TABLE "SymbolProfile" ADD COLUMN "assetClass" "AssetClass";
+ALTER TABLE "SymbolProfile" ADD COLUMN "assetClass" "AssetClass";
diff --git a/prisma/migrations/20210815180121_added_settings_to_settings/migration.sql b/prisma/migrations/20210815180121_added_settings_to_settings/migration.sql
index da863fb73..2e7c357f3 100644
--- a/prisma/migrations/20210815180121_added_settings_to_settings/migration.sql
+++ b/prisma/migrations/20210815180121_added_settings_to_settings/migration.sql
@@ -1,2 +1,2 @@
-- AlterTable
-ALTER TABLE "Settings" ADD COLUMN "settings" JSONB;
+ALTER TABLE "Settings" ADD COLUMN "settings" JSONB;
diff --git a/prisma/migrations/20210822200534_added_asset_sub_class_to_symbol_profile/migration.sql b/prisma/migrations/20210822200534_added_asset_sub_class_to_symbol_profile/migration.sql
index ca1ad3d26..7d1122c88 100644
--- a/prisma/migrations/20210822200534_added_asset_sub_class_to_symbol_profile/migration.sql
+++ b/prisma/migrations/20210822200534_added_asset_sub_class_to_symbol_profile/migration.sql
@@ -2,4 +2,4 @@
CREATE TYPE "AssetSubClass" AS ENUM ('CRYPTOCURRENCY', 'ETF', 'STOCK');
-- AlterTable
-ALTER TABLE "SymbolProfile" ADD COLUMN "assetSubClass" "AssetSubClass";
+ALTER TABLE "SymbolProfile" ADD COLUMN "assetSubClass" "AssetSubClass";
diff --git a/prisma/migrations/20210916182355_added_data_source_to_market_data/migration.sql b/prisma/migrations/20210916182355_added_data_source_to_market_data/migration.sql
index 83911236a..720460f57 100644
--- a/prisma/migrations/20210916182355_added_data_source_to_market_data/migration.sql
+++ b/prisma/migrations/20210916182355_added_data_source_to_market_data/migration.sql
@@ -1,2 +1,2 @@
-- AlterTable
-ALTER TABLE "MarketData" ADD COLUMN "dataSource" "DataSource" NOT NULL DEFAULT E'YAHOO';
+ALTER TABLE "MarketData" ADD COLUMN "dataSource" "DataSource" NOT NULL DEFAULT E'YAHOO';
diff --git a/prisma/migrations/20211107082008_added_symbol_mapping_to_symbol_profile/migration.sql b/prisma/migrations/20211107082008_added_symbol_mapping_to_symbol_profile/migration.sql
index a6cdeb92c..07f6a3b3d 100644
--- a/prisma/migrations/20211107082008_added_symbol_mapping_to_symbol_profile/migration.sql
+++ b/prisma/migrations/20211107082008_added_symbol_mapping_to_symbol_profile/migration.sql
@@ -1,2 +1,2 @@
-- AlterTable
-ALTER TABLE "SymbolProfile" ADD COLUMN "symbolMapping" JSONB;
+ALTER TABLE "SymbolProfile" ADD COLUMN "symbolMapping" JSONB;
diff --git a/prisma/migrations/20211107171624_added_scraper_configuration_to_symbol_profile/migration.sql b/prisma/migrations/20211107171624_added_scraper_configuration_to_symbol_profile/migration.sql
index 84ca4bb96..ab32cddef 100644
--- a/prisma/migrations/20211107171624_added_scraper_configuration_to_symbol_profile/migration.sql
+++ b/prisma/migrations/20211107171624_added_scraper_configuration_to_symbol_profile/migration.sql
@@ -1,2 +1,2 @@
-- AlterTable
-ALTER TABLE "SymbolProfile" ADD COLUMN "scraperConfiguration" JSONB;
+ALTER TABLE "SymbolProfile" ADD COLUMN "scraperConfiguration" JSONB;
diff --git a/prisma/migrations/20220227093650_added_url_to_symbol_profile/migration.sql b/prisma/migrations/20220227093650_added_url_to_symbol_profile/migration.sql
index 61456216c..b5781f8d1 100644
--- a/prisma/migrations/20220227093650_added_url_to_symbol_profile/migration.sql
+++ b/prisma/migrations/20220227093650_added_url_to_symbol_profile/migration.sql
@@ -1,2 +1,2 @@
-- AlterTable
-ALTER TABLE "SymbolProfile" ADD COLUMN "url" TEXT;
+ALTER TABLE "SymbolProfile" ADD COLUMN "url" TEXT;
From f01a3f893d89e02756d46e08e11d9ef89633d089 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sun, 25 Sep 2022 18:02:46 +0200
Subject: [PATCH 11/29] Exclude accounts (#1289)
* Exclude accounts
* Update changelog
---
CHANGELOG.md | 6 +
.../api/src/app/account/account.controller.ts | 7 +-
apps/api/src/app/account/account.service.ts | 12 +-
.../api/src/app/account/create-account.dto.ts | 12 +-
.../api/src/app/account/update-account.dto.ts | 12 +-
apps/api/src/app/order/order.controller.ts | 3 +-
apps/api/src/app/order/order.service.ts | 42 ++--
.../src/app/portfolio/portfolio.controller.ts | 64 ++---
.../src/app/portfolio/portfolio.service.ts | 222 ++++++++++++------
.../account-detail-dialog.component.ts | 2 +-
.../account-detail-dialog.html | 6 +-
.../home-summary/home-summary.component.ts | 50 +++-
.../portfolio-summary.component.html | 11 +
.../pages/accounts/accounts-page.component.ts | 7 +-
.../create-or-update-account-dialog.html | 8 +
.../create-or-update-account-dialog.module.ts | 2 +
.../portfolio/fire/fire-page.component.ts | 8 +-
apps/client/src/app/services/data.service.ts | 36 +--
.../interfaces/portfolio-details.interface.ts | 6 +-
.../interfaces/portfolio-summary.interface.ts | 3 +-
.../migration.sql | 2 +
prisma/schema.prisma | 1 +
22 files changed, 345 insertions(+), 177 deletions(-)
create mode 100644 prisma/migrations/20220924175215_added_is_excluded_to_account/migration.sql
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20277377b..6fd6c7129 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## Unreleased
+
+### Added
+
+- Added support to exclude an account from analysis
+
## 1.197.0 - 24.09.2022
### Added
diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts
index 524e36f5a..3ef671cc4 100644
--- a/apps/api/src/app/account/account.controller.ts
+++ b/apps/api/src/app/account/account.controller.ts
@@ -96,7 +96,9 @@ export class AccountController {
let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations(
- impersonationUserId || this.request.user.id
+ impersonationUserId || this.request.user.id,
+ undefined,
+ true
);
if (
@@ -139,7 +141,8 @@ export class AccountController {
let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations(
impersonationUserId || this.request.user.id,
- [{ id, type: 'ACCOUNT' }]
+ [{ id, type: 'ACCOUNT' }],
+ true
);
if (
diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts
index b9b65716a..7c10fc31f 100644
--- a/apps/api/src/app/account/account.service.ts
+++ b/apps/api/src/app/account/account.service.ts
@@ -107,15 +107,23 @@ export class AccountService {
public async getCashDetails({
currency,
filters = [],
- userId
+ userId,
+ withExcludedAccounts = false
}: {
currency: string;
filters?: Filter[];
userId: string;
+ withExcludedAccounts?: boolean;
}): Promise {
let totalCashBalanceInBaseCurrency = new Big(0);
- const where: Prisma.AccountWhereInput = { userId };
+ const where: Prisma.AccountWhereInput = {
+ userId
+ };
+
+ if (withExcludedAccounts === false) {
+ where.isExcluded = false;
+ }
const {
ACCOUNT: filtersByAccount,
diff --git a/apps/api/src/app/account/create-account.dto.ts b/apps/api/src/app/account/create-account.dto.ts
index f53a20e76..3ea13e20a 100644
--- a/apps/api/src/app/account/create-account.dto.ts
+++ b/apps/api/src/app/account/create-account.dto.ts
@@ -1,5 +1,11 @@
import { AccountType } from '@prisma/client';
-import { IsNumber, IsString, ValidateIf } from 'class-validator';
+import {
+ IsBoolean,
+ IsNumber,
+ IsOptional,
+ IsString,
+ ValidateIf
+} from 'class-validator';
export class CreateAccountDto {
@IsString()
@@ -11,6 +17,10 @@ export class CreateAccountDto {
@IsString()
currency: string;
+ @IsBoolean()
+ @IsOptional()
+ isExcluded?: boolean;
+
@IsString()
name: string;
diff --git a/apps/api/src/app/account/update-account.dto.ts b/apps/api/src/app/account/update-account.dto.ts
index 343f46a7a..0b5737607 100644
--- a/apps/api/src/app/account/update-account.dto.ts
+++ b/apps/api/src/app/account/update-account.dto.ts
@@ -1,5 +1,11 @@
import { AccountType } from '@prisma/client';
-import { IsNumber, IsString, ValidateIf } from 'class-validator';
+import {
+ IsBoolean,
+ IsNumber,
+ IsOptional,
+ IsString,
+ ValidateIf
+} from 'class-validator';
export class UpdateAccountDto {
@IsString()
@@ -14,6 +20,10 @@ export class UpdateAccountDto {
@IsString()
id: string;
+ @IsBoolean()
+ @IsOptional()
+ isExcluded?: boolean;
+
@IsString()
name: string;
diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts
index e3cafefca..a0c606b8c 100644
--- a/apps/api/src/app/order/order.controller.ts
+++ b/apps/api/src/app/order/order.controller.ts
@@ -109,7 +109,8 @@ export class OrderController {
filters,
userCurrency,
includeDrafts: true,
- userId: impersonationUserId || this.request.user.id
+ userId: impersonationUserId || this.request.user.id,
+ withExcludedAccounts: true
});
if (
diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts
index bf549200e..b95c96975 100644
--- a/apps/api/src/app/order/order.service.ts
+++ b/apps/api/src/app/order/order.service.ts
@@ -189,13 +189,15 @@ export class OrderService {
includeDrafts = false,
types,
userCurrency,
- userId
+ userId,
+ withExcludedAccounts = false
}: {
filters?: Filter[];
includeDrafts?: boolean;
types?: TypeOfOrder[];
userCurrency: string;
userId: string;
+ withExcludedAccounts?: boolean;
}): Promise {
const where: Prisma.OrderWhereInput = { userId };
@@ -284,24 +286,28 @@ export class OrderService {
},
orderBy: { date: 'asc' }
})
- ).map((order) => {
- const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
-
- return {
- ...order,
- value,
- feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
- order.fee,
- order.SymbolProfile.currency,
- userCurrency
- ),
- valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
+ )
+ .filter((order) => {
+ return withExcludedAccounts || order.Account?.isExcluded === false;
+ })
+ .map((order) => {
+ const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
+
+ return {
+ ...order,
value,
- order.SymbolProfile.currency,
- userCurrency
- )
- };
- });
+ feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
+ order.fee,
+ order.SymbolProfile.currency,
+ userCurrency
+ ),
+ valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
+ value,
+ order.SymbolProfile.currency,
+ userCurrency
+ )
+ };
+ });
}
public async updateOrder({
diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts
index 66cd408cc..4a017388f 100644
--- a/apps/api/src/app/portfolio/portfolio.controller.ts
+++ b/apps/api/src/app/portfolio/portfolio.controller.ts
@@ -148,12 +148,15 @@ export class PortfolioController {
})
];
+ let portfolioSummary: PortfolioSummary;
+
const {
accounts,
filteredValueInBaseCurrency,
filteredValueInPercentage,
hasErrors,
holdings,
+ summary,
totalValueInBaseCurrency
} = await this.portfolioService.getDetails(
impersonationId,
@@ -166,6 +169,8 @@ export class PortfolioController {
hasError = true;
}
+ portfolioSummary = summary;
+
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
@@ -199,6 +204,22 @@ export class PortfolioController {
accounts[name].current = current / totalValue;
accounts[name].original = original / totalInvestment;
}
+
+ portfolioSummary = nullifyValuesInObject(summary, [
+ 'cash',
+ 'committedFunds',
+ 'currentGrossPerformance',
+ 'currentNetPerformance',
+ 'currentValue',
+ 'dividend',
+ 'emergencyFund',
+ 'excludedAccountsAndActivities',
+ 'fees',
+ 'items',
+ 'netWorth',
+ 'totalBuy',
+ 'totalSell'
+ ]);
}
let hasDetails = true;
@@ -224,7 +245,8 @@ export class PortfolioController {
filteredValueInPercentage,
hasError,
holdings,
- totalValueInBaseCurrency
+ totalValueInBaseCurrency,
+ summary: hasDetails ? portfolioSummary : undefined
};
}
@@ -420,46 +442,6 @@ export class PortfolioController {
return portfolioPublicDetails;
}
- @Get('summary')
- @UseGuards(AuthGuard('jwt'))
- public async getSummary(
- @Headers('impersonation-id') impersonationId
- ): Promise {
- if (
- this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
- this.request.user.subscription.type === 'Basic'
- ) {
- throw new HttpException(
- getReasonPhrase(StatusCodes.FORBIDDEN),
- StatusCodes.FORBIDDEN
- );
- }
-
- let summary = await this.portfolioService.getSummary(impersonationId);
-
- if (
- impersonationId ||
- this.userService.isRestrictedView(this.request.user)
- ) {
- summary = nullifyValuesInObject(summary, [
- 'cash',
- 'committedFunds',
- 'currentGrossPerformance',
- 'currentNetPerformance',
- 'currentValue',
- 'dividend',
- 'emergencyFund',
- 'fees',
- 'items',
- 'netWorth',
- 'totalBuy',
- 'totalSell'
- ]);
- }
-
- return summary;
- }
-
@Get('position/:dataSource/:symbol')
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts
index c38e2a8d0..a49bda912 100644
--- a/apps/api/src/app/portfolio/portfolio.service.ts
+++ b/apps/api/src/app/portfolio/portfolio.service.ts
@@ -50,8 +50,11 @@ import type {
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import {
+ Account,
AssetClass,
DataSource,
+ Order,
+ Platform,
Prisma,
Tag,
Type as TypeOfOrder
@@ -106,7 +109,8 @@ export class PortfolioService {
public async getAccounts(
aUserId: string,
- aFilters?: Filter[]
+ aFilters?: Filter[],
+ withExcludedAccounts = false
): Promise {
const where: Prisma.AccountWhereInput = { userId: aUserId };
@@ -120,7 +124,13 @@ export class PortfolioService {
include: { Order: true, Platform: true },
orderBy: { name: 'asc' }
}),
- this.getDetails(aUserId, aUserId, undefined, aFilters)
+ this.getDetails(
+ aUserId,
+ aUserId,
+ undefined,
+ aFilters,
+ withExcludedAccounts
+ )
]);
const userCurrency = this.request.user.Settings.settings.baseCurrency;
@@ -160,9 +170,14 @@ export class PortfolioService {
public async getAccountsWithAggregations(
aUserId: string,
- aFilters?: Filter[]
+ aFilters?: Filter[],
+ withExcludedAccounts = false
): Promise {
- const accounts = await this.getAccounts(aUserId, aFilters);
+ const accounts = await this.getAccounts(
+ aUserId,
+ aFilters,
+ withExcludedAccounts
+ );
let totalBalanceInBaseCurrency = new Big(0);
let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0;
@@ -410,7 +425,8 @@ export class PortfolioService {
aImpersonationId: string,
aUserId: string,
aDateRange: DateRange = 'max',
- aFilters?: Filter[]
+ aFilters?: Filter[],
+ withExcludedAccounts = false
): Promise {
const userId = await this.getUserId(aImpersonationId, aUserId);
const user = await this.userService.user({ id: userId });
@@ -426,6 +442,7 @@ export class PortfolioService {
const { orders, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
userId,
+ withExcludedAccounts,
filters: aFilters
});
@@ -580,6 +597,7 @@ export class PortfolioService {
portfolioItemsNow,
userCurrency,
userId,
+ withExcludedAccounts,
filters: aFilters
});
@@ -588,6 +606,7 @@ export class PortfolioService {
return {
accounts,
holdings,
+ summary,
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: summary.netWorth
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
@@ -606,7 +625,11 @@ export class PortfolioService {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const orders = (
- await this.orderService.getOrders({ userCurrency, userId })
+ await this.orderService.getOrders({
+ userCurrency,
+ userId,
+ withExcludedAccounts: true
+ })
).filter(({ SymbolProfile }) => {
return (
SymbolProfile.dataSource === aDataSource &&
@@ -1181,74 +1204,6 @@ export class PortfolioService {
};
}
- public async getSummary(aImpersonationId: string): Promise {
- const userCurrency = this.request.user.Settings.settings.baseCurrency;
- const userId = await this.getUserId(aImpersonationId, this.request.user.id);
- const user = await this.userService.user({ id: userId });
-
- const performanceInformation = await this.getPerformance(aImpersonationId);
-
- const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
- userId,
- currency: userCurrency
- });
- const orders = await this.orderService.getOrders({
- userCurrency,
- userId
- });
- const dividend = this.getDividend(orders).toNumber();
- const emergencyFund = new Big(
- (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
- );
- const fees = this.getFees(orders).toNumber();
- const firstOrderDate = orders[0]?.date;
- const items = this.getItems(orders).toNumber();
-
- const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
- const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
-
- const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
- const committedFunds = new Big(totalBuy).minus(totalSell);
-
- const netWorth = new Big(balanceInBaseCurrency)
- .plus(performanceInformation.performance.currentValue)
- .plus(items)
- .toNumber();
-
- const daysInMarket = differenceInDays(new Date(), firstOrderDate);
-
- const annualizedPerformancePercent = new PortfolioCalculator({
- currency: userCurrency,
- currentRateService: this.currentRateService,
- orders: []
- })
- .getAnnualizedPerformancePercent({
- daysInMarket,
- netPerformancePercent: new Big(
- performanceInformation.performance.currentNetPerformancePercent
- )
- })
- ?.toNumber();
-
- return {
- ...performanceInformation.performance,
- annualizedPerformancePercent,
- cash,
- dividend,
- fees,
- firstOrderDate,
- items,
- netWorth,
- totalBuy,
- totalSell,
- committedFunds: committedFunds.toNumber(),
- emergencyFund: emergencyFund.toNumber(),
- ordersCount: orders.filter((order) => {
- return order.type === 'BUY' || order.type === 'SELL';
- }).length
- };
- }
-
private async getCashPositions({
cashDetails,
emergencyFund,
@@ -1424,14 +1379,117 @@ export class PortfolioService {
return portfolioStart;
}
+ private async getSummary(
+ aImpersonationId: string
+ ): Promise {
+ const userCurrency = this.request.user.Settings.settings.baseCurrency;
+ const userId = await this.getUserId(aImpersonationId, this.request.user.id);
+ const user = await this.userService.user({ id: userId });
+
+ const performanceInformation = await this.getPerformance(aImpersonationId);
+
+ const { balanceInBaseCurrency } = await this.accountService.getCashDetails({
+ userId,
+ currency: userCurrency
+ });
+ const orders = await this.orderService.getOrders({
+ userCurrency,
+ userId
+ });
+
+ const excludedActivities = (
+ await this.orderService.getOrders({
+ userCurrency,
+ userId,
+ withExcludedAccounts: true
+ })
+ ).filter(({ Account: account }) => {
+ return account?.isExcluded ?? false;
+ });
+
+ const dividend = this.getDividend(orders).toNumber();
+ const emergencyFund = new Big(
+ (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
+ );
+ const fees = this.getFees(orders).toNumber();
+ const firstOrderDate = orders[0]?.date;
+ const items = this.getItems(orders).toNumber();
+
+ const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
+ const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
+
+ const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
+ const committedFunds = new Big(totalBuy).minus(totalSell);
+ const totalOfExcludedActivities = new Big(
+ this.getTotalByType(excludedActivities, userCurrency, 'BUY')
+ ).minus(this.getTotalByType(excludedActivities, userCurrency, 'SELL'));
+
+ const cashDetailsWithExcludedAccounts =
+ await this.accountService.getCashDetails({
+ userId,
+ currency: userCurrency,
+ withExcludedAccounts: true
+ });
+
+ const excludedBalanceInBaseCurrency = new Big(
+ cashDetailsWithExcludedAccounts.balanceInBaseCurrency
+ ).minus(balanceInBaseCurrency);
+
+ const excludedAccountsAndActivities = excludedBalanceInBaseCurrency
+ .plus(totalOfExcludedActivities)
+ .toNumber();
+
+ const netWorth = new Big(balanceInBaseCurrency)
+ .plus(performanceInformation.performance.currentValue)
+ .plus(items)
+ .plus(excludedAccountsAndActivities)
+ .toNumber();
+
+ const daysInMarket = differenceInDays(new Date(), firstOrderDate);
+
+ const annualizedPerformancePercent = new PortfolioCalculator({
+ currency: userCurrency,
+ currentRateService: this.currentRateService,
+ orders: []
+ })
+ .getAnnualizedPerformancePercent({
+ daysInMarket,
+ netPerformancePercent: new Big(
+ performanceInformation.performance.currentNetPerformancePercent
+ )
+ })
+ ?.toNumber();
+
+ return {
+ ...performanceInformation.performance,
+ annualizedPerformancePercent,
+ cash,
+ dividend,
+ excludedAccountsAndActivities,
+ fees,
+ firstOrderDate,
+ items,
+ netWorth,
+ totalBuy,
+ totalSell,
+ committedFunds: committedFunds.toNumber(),
+ emergencyFund: emergencyFund.toNumber(),
+ ordersCount: orders.filter((order) => {
+ return order.type === 'BUY' || order.type === 'SELL';
+ }).length
+ };
+ }
+
private async getTransactionPoints({
filters,
includeDrafts = false,
- userId
+ userId,
+ withExcludedAccounts
}: {
filters?: Filter[];
includeDrafts?: boolean;
userId: string;
+ withExcludedAccounts?: boolean;
}): Promise<{
transactionPoints: TransactionPoint[];
orders: OrderWithAccount[];
@@ -1445,6 +1503,7 @@ export class PortfolioService {
includeDrafts,
userCurrency,
userId,
+ withExcludedAccounts,
types: ['BUY', 'SELL']
});
@@ -1496,17 +1555,22 @@ export class PortfolioService {
orders,
portfolioItemsNow,
userCurrency,
- userId
+ userId,
+ withExcludedAccounts
}: {
filters?: Filter[];
orders: OrderWithAccount[];
portfolioItemsNow: { [p: string]: TimelinePosition };
userCurrency: string;
userId: string;
+ withExcludedAccounts?: boolean;
}) {
const accounts: PortfolioDetails['accounts'] = {};
- let currentAccounts = [];
+ let currentAccounts: (Account & {
+ Order?: Order[];
+ Platform?: Platform;
+ })[] = [];
if (filters.length === 0) {
currentAccounts = await this.accountService.getAccounts(userId);
@@ -1526,6 +1590,10 @@ export class PortfolioService {
});
}
+ currentAccounts = currentAccounts.filter((account) => {
+ return withExcludedAccounts || account.isExcluded === false;
+ });
+
for (const account of currentAccounts) {
const ordersByAccount = orders.filter(({ accountId }) => {
return accountId === account.id;
diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
index ca2f229e7..a0c11bce8 100644
--- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
+++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
@@ -61,7 +61,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
this.accountType = accountType;
this.name = name;
- this.platformName = Platform?.name;
+ this.platformName = Platform?.name ?? '-';
this.valueInBaseCurrency = valueInBaseCurrency;
this.changeDetectorRef.markForCheck();
diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
index edf111ffc..53a9c76ea 100644
--- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
+++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
@@ -21,10 +21,12 @@
- Account Type
+ Account Type
- Platform
+ Platform
diff --git a/apps/client/src/app/components/home-summary/home-summary.component.ts b/apps/client/src/app/components/home-summary/home-summary.component.ts
index 106aba6c9..f10f09fe3 100644
--- a/apps/client/src/app/components/home-summary/home-summary.component.ts
+++ b/apps/client/src/app/components/home-summary/home-summary.component.ts
@@ -1,8 +1,18 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
+import {
+ MatSnackBar,
+ MatSnackBarRef,
+ TextOnlySnackBar
+} from '@angular/material/snack-bar';
+import { Router } from '@angular/router';
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 { PortfolioSummary, User } from '@ghostfolio/common/interfaces';
+import {
+ InfoItem,
+ PortfolioSummary,
+ User
+} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@@ -14,8 +24,11 @@ import { takeUntil } from 'rxjs/operators';
})
export class HomeSummaryComponent implements OnDestroy, OnInit {
public hasImpersonationId: boolean;
+ public hasPermissionForSubscription: boolean;
public hasPermissionToUpdateUserSettings: boolean;
+ public info: InfoItem;
public isLoading = true;
+ public snackBarRef: MatSnackBarRef;
public summary: PortfolioSummary;
public user: User;
@@ -25,8 +38,17 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private impersonationStorageService: ImpersonationStorageService,
+ private router: Router,
+ private snackBar: MatSnackBar,
private userService: UserService
) {
+ this.info = this.dataService.fetchInfo();
+
+ this.hasPermissionForSubscription = hasPermission(
+ this.info?.globalPermissions,
+ permissions.enableSubscription
+ );
+
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
@@ -50,8 +72,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
.subscribe((aId) => {
this.hasImpersonationId = !!aId;
});
-
- this.update();
}
public onChangeEmergencyFund(emergencyFund: number) {
@@ -81,12 +101,30 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
this.isLoading = true;
this.dataService
- .fetchPortfolioSummary()
+ .fetchPortfolioDetails({})
.pipe(takeUntil(this.unsubscribeSubject))
- .subscribe((response) => {
- this.summary = response;
+ .subscribe(({ summary }) => {
+ this.summary = summary;
this.isLoading = false;
+ if (!this.summary) {
+ this.snackBarRef = this.snackBar.open(
+ $localize`This feature requires a subscription.`,
+ this.hasPermissionForSubscription
+ ? $localize`Upgrade Plan`
+ : undefined,
+ { duration: 6000 }
+ );
+
+ this.snackBarRef.afterDismissed().subscribe(() => {
+ this.snackBarRef = undefined;
+ });
+
+ this.snackBarRef.onAction().subscribe(() => {
+ this.router.navigate(['/pricing']);
+ });
+ }
+
this.changeDetectorRef.markForCheck();
});
diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
index 577d41741..78e30675c 100644
--- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
+++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
@@ -172,6 +172,17 @@
>
+
+
Excluded from Analysis
+
+
+
+
diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts
index 81c02c2fd..6d7f84555 100644
--- a/apps/client/src/app/pages/accounts/accounts-page.component.ts
+++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts
@@ -59,8 +59,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.openCreateAccountDialog();
} else if (params['editDialog']) {
if (this.accounts) {
- const account = this.accounts.find((account) => {
- return account.id === params['accountId'];
+ const account = this.accounts.find(({ id }) => {
+ return id === params['accountId'];
});
this.openUpdateAccountDialog(account);
@@ -155,6 +155,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
balance,
currency,
id,
+ isExcluded,
name,
platformId
}: AccountModel): void {
@@ -165,6 +166,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
balance,
currency,
id,
+ isExcluded,
name,
platformId
}
@@ -231,6 +233,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
accountType: AccountType.SECURITIES,
balance: 0,
currency: this.user?.settings?.baseCurrency,
+ isExcluded: false,
name: null,
platformId: null
}
diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
index 971487356..ba2a1cee2 100644
--- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
+++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
@@ -50,6 +50,14 @@
+