Browse Source

Merge branch 'main' of https://github.com/m11tch/ghostfolio into main

pull/1295/head
m11tch 3 years ago
parent
commit
8f09ebfb4a
  1. 7
      CHANGELOG.md
  2. 7
      apps/api/src/app/account/account.controller.ts
  3. 12
      apps/api/src/app/account/account.service.ts
  4. 12
      apps/api/src/app/account/create-account.dto.ts
  5. 12
      apps/api/src/app/account/update-account.dto.ts
  6. 3
      apps/api/src/app/order/order.controller.ts
  7. 42
      apps/api/src/app/order/order.service.ts
  8. 64
      apps/api/src/app/portfolio/portfolio.controller.ts
  9. 222
      apps/api/src/app/portfolio/portfolio.service.ts
  10. 2
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  11. 6
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  12. 50
      apps/client/src/app/components/home-summary/home-summary.component.ts
  13. 11
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html
  14. 7
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  15. 8
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html
  16. 2
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.module.ts
  17. 8
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  18. 36
      apps/client/src/app/services/data.service.ts
  19. 6
      libs/common/src/lib/interfaces/portfolio-details.interface.ts
  20. 3
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts
  21. 2
      package.json
  22. 2
      prisma/migrations/20210605161257_added_symbol_profile/migration.sql
  23. 2
      prisma/migrations/20210612110542_added_auth_device/migration.sql
  24. 2
      prisma/migrations/20210616075245_added_sectors_to_symbol_profile/migration.sql
  25. 4
      prisma/migrations/20210703194509_added_balance_to_account/migration.sql
  26. 2
      prisma/migrations/20210724160404_added_currency_to_symbol_profile/migration.sql
  27. 2
      prisma/migrations/20210807062952_added_is_draft_to_order/migration.sql
  28. 2
      prisma/migrations/20210808075949_added_asset_class_to_symbol_profile/migration.sql
  29. 2
      prisma/migrations/20210815180121_added_settings_to_settings/migration.sql
  30. 2
      prisma/migrations/20210822200534_added_asset_sub_class_to_symbol_profile/migration.sql
  31. 2
      prisma/migrations/20210916182355_added_data_source_to_market_data/migration.sql
  32. 2
      prisma/migrations/20211107082008_added_symbol_mapping_to_symbol_profile/migration.sql
  33. 2
      prisma/migrations/20211107171624_added_scraper_configuration_to_symbol_profile/migration.sql
  34. 2
      prisma/migrations/20220227093650_added_url_to_symbol_profile/migration.sql
  35. 2
      prisma/migrations/20220924175215_added_is_excluded_to_account/migration.sql
  36. 1
      prisma/schema.prisma

7
CHANGELOG.md

@ -5,6 +5,13 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.198.0 - 25.09.2022
### Added
- Added support to exclude an account from analysis
- Set up the language localization for Nederlands (`nl`)
## 1.197.0 - 24.09.2022 ## 1.197.0 - 24.09.2022
### Added ### Added

7
apps/api/src/app/account/account.controller.ts

@ -96,7 +96,9 @@ export class AccountController {
let accountsWithAggregations = let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations( await this.portfolioService.getAccountsWithAggregations(
impersonationUserId || this.request.user.id impersonationUserId || this.request.user.id,
undefined,
true
); );
if ( if (
@ -139,7 +141,8 @@ export class AccountController {
let accountsWithAggregations = let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations( await this.portfolioService.getAccountsWithAggregations(
impersonationUserId || this.request.user.id, impersonationUserId || this.request.user.id,
[{ id, type: 'ACCOUNT' }] [{ id, type: 'ACCOUNT' }],
true
); );
if ( if (

12
apps/api/src/app/account/account.service.ts

@ -107,15 +107,23 @@ export class AccountService {
public async getCashDetails({ public async getCashDetails({
currency, currency,
filters = [], filters = [],
userId userId,
withExcludedAccounts = false
}: { }: {
currency: string; currency: string;
filters?: Filter[]; filters?: Filter[];
userId: string; userId: string;
withExcludedAccounts?: boolean;
}): Promise<CashDetails> { }): Promise<CashDetails> {
let totalCashBalanceInBaseCurrency = new Big(0); let totalCashBalanceInBaseCurrency = new Big(0);
const where: Prisma.AccountWhereInput = { userId }; const where: Prisma.AccountWhereInput = {
userId
};
if (withExcludedAccounts === false) {
where.isExcluded = false;
}
const { const {
ACCOUNT: filtersByAccount, ACCOUNT: filtersByAccount,

12
apps/api/src/app/account/create-account.dto.ts

@ -1,5 +1,11 @@
import { AccountType } from '@prisma/client'; import { AccountType } from '@prisma/client';
import { IsNumber, IsString, ValidateIf } from 'class-validator'; import {
IsBoolean,
IsNumber,
IsOptional,
IsString,
ValidateIf
} from 'class-validator';
export class CreateAccountDto { export class CreateAccountDto {
@IsString() @IsString()
@ -11,6 +17,10 @@ export class CreateAccountDto {
@IsString() @IsString()
currency: string; currency: string;
@IsBoolean()
@IsOptional()
isExcluded?: boolean;
@IsString() @IsString()
name: string; name: string;

12
apps/api/src/app/account/update-account.dto.ts

@ -1,5 +1,11 @@
import { AccountType } from '@prisma/client'; import { AccountType } from '@prisma/client';
import { IsNumber, IsString, ValidateIf } from 'class-validator'; import {
IsBoolean,
IsNumber,
IsOptional,
IsString,
ValidateIf
} from 'class-validator';
export class UpdateAccountDto { export class UpdateAccountDto {
@IsString() @IsString()
@ -14,6 +20,10 @@ export class UpdateAccountDto {
@IsString() @IsString()
id: string; id: string;
@IsBoolean()
@IsOptional()
isExcluded?: boolean;
@IsString() @IsString()
name: string; name: string;

3
apps/api/src/app/order/order.controller.ts

@ -109,7 +109,8 @@ export class OrderController {
filters, filters,
userCurrency, userCurrency,
includeDrafts: true, includeDrafts: true,
userId: impersonationUserId || this.request.user.id userId: impersonationUserId || this.request.user.id,
withExcludedAccounts: true
}); });
if ( if (

42
apps/api/src/app/order/order.service.ts

@ -189,13 +189,15 @@ export class OrderService {
includeDrafts = false, includeDrafts = false,
types, types,
userCurrency, userCurrency,
userId userId,
withExcludedAccounts = false
}: { }: {
filters?: Filter[]; filters?: Filter[];
includeDrafts?: boolean; includeDrafts?: boolean;
types?: TypeOfOrder[]; types?: TypeOfOrder[];
userCurrency: string; userCurrency: string;
userId: string; userId: string;
withExcludedAccounts?: boolean;
}): Promise<Activity[]> { }): Promise<Activity[]> {
const where: Prisma.OrderWhereInput = { userId }; const where: Prisma.OrderWhereInput = { userId };
@ -284,24 +286,28 @@ export class OrderService {
}, },
orderBy: { date: 'asc' } orderBy: { date: 'asc' }
}) })
).map((order) => { )
const value = new Big(order.quantity).mul(order.unitPrice).toNumber(); .filter((order) => {
return withExcludedAccounts || order.Account?.isExcluded === false;
return { })
...order, .map((order) => {
value, const value = new Big(order.quantity).mul(order.unitPrice).toNumber();
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
order.fee, return {
order.SymbolProfile.currency, ...order,
userCurrency
),
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value, value,
order.SymbolProfile.currency, feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
userCurrency order.fee,
) order.SymbolProfile.currency,
}; userCurrency
}); ),
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
value,
order.SymbolProfile.currency,
userCurrency
)
};
});
} }
public async updateOrder({ public async updateOrder({

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

@ -148,12 +148,15 @@ export class PortfolioController {
}) })
]; ];
let portfolioSummary: PortfolioSummary;
const { const {
accounts, accounts,
filteredValueInBaseCurrency, filteredValueInBaseCurrency,
filteredValueInPercentage, filteredValueInPercentage,
hasErrors, hasErrors,
holdings, holdings,
summary,
totalValueInBaseCurrency totalValueInBaseCurrency
} = await this.portfolioService.getDetails( } = await this.portfolioService.getDetails(
impersonationId, impersonationId,
@ -166,6 +169,8 @@ export class PortfolioController {
hasError = true; hasError = true;
} }
portfolioSummary = summary;
if ( if (
impersonationId || impersonationId ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
@ -199,6 +204,22 @@ export class PortfolioController {
accounts[name].current = current / totalValue; accounts[name].current = current / totalValue;
accounts[name].original = original / totalInvestment; accounts[name].original = original / totalInvestment;
} }
portfolioSummary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'dividend',
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
'items',
'netWorth',
'totalBuy',
'totalSell'
]);
} }
let hasDetails = true; let hasDetails = true;
@ -224,7 +245,8 @@ export class PortfolioController {
filteredValueInPercentage, filteredValueInPercentage,
hasError, hasError,
holdings, holdings,
totalValueInBaseCurrency totalValueInBaseCurrency,
summary: hasDetails ? portfolioSummary : undefined
}; };
} }
@ -420,46 +442,6 @@ export class PortfolioController {
return portfolioPublicDetails; return portfolioPublicDetails;
} }
@Get('summary')
@UseGuards(AuthGuard('jwt'))
public async getSummary(
@Headers('impersonation-id') impersonationId
): Promise<PortfolioSummary> {
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') @Get('position/:dataSource/:symbol')
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)

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

@ -50,8 +50,11 @@ import type {
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { import {
Account,
AssetClass, AssetClass,
DataSource, DataSource,
Order,
Platform,
Prisma, Prisma,
Tag, Tag,
Type as TypeOfOrder Type as TypeOfOrder
@ -106,7 +109,8 @@ export class PortfolioService {
public async getAccounts( public async getAccounts(
aUserId: string, aUserId: string,
aFilters?: Filter[] aFilters?: Filter[],
withExcludedAccounts = false
): Promise<AccountWithValue[]> { ): Promise<AccountWithValue[]> {
const where: Prisma.AccountWhereInput = { userId: aUserId }; const where: Prisma.AccountWhereInput = { userId: aUserId };
@ -120,7 +124,13 @@ export class PortfolioService {
include: { Order: true, Platform: true }, include: { Order: true, Platform: true },
orderBy: { name: 'asc' } orderBy: { name: 'asc' }
}), }),
this.getDetails(aUserId, aUserId, undefined, aFilters) this.getDetails(
aUserId,
aUserId,
undefined,
aFilters,
withExcludedAccounts
)
]); ]);
const userCurrency = this.request.user.Settings.settings.baseCurrency; const userCurrency = this.request.user.Settings.settings.baseCurrency;
@ -160,9 +170,14 @@ export class PortfolioService {
public async getAccountsWithAggregations( public async getAccountsWithAggregations(
aUserId: string, aUserId: string,
aFilters?: Filter[] aFilters?: Filter[],
withExcludedAccounts = false
): Promise<Accounts> { ): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId, aFilters); const accounts = await this.getAccounts(
aUserId,
aFilters,
withExcludedAccounts
);
let totalBalanceInBaseCurrency = new Big(0); let totalBalanceInBaseCurrency = new Big(0);
let totalValueInBaseCurrency = new Big(0); let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0; let transactionCount = 0;
@ -410,7 +425,8 @@ export class PortfolioService {
aImpersonationId: string, aImpersonationId: string,
aUserId: string, aUserId: string,
aDateRange: DateRange = 'max', aDateRange: DateRange = 'max',
aFilters?: Filter[] aFilters?: Filter[],
withExcludedAccounts = false
): Promise<PortfolioDetails & { hasErrors: boolean }> { ): Promise<PortfolioDetails & { hasErrors: boolean }> {
const userId = await this.getUserId(aImpersonationId, aUserId); const userId = await this.getUserId(aImpersonationId, aUserId);
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
@ -426,6 +442,7 @@ export class PortfolioService {
const { orders, portfolioOrders, transactionPoints } = const { orders, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({ await this.getTransactionPoints({
userId, userId,
withExcludedAccounts,
filters: aFilters filters: aFilters
}); });
@ -580,6 +597,7 @@ export class PortfolioService {
portfolioItemsNow, portfolioItemsNow,
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts,
filters: aFilters filters: aFilters
}); });
@ -588,6 +606,7 @@ export class PortfolioService {
return { return {
accounts, accounts,
holdings, holdings,
summary,
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: summary.netWorth filteredValueInPercentage: summary.netWorth
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber() ? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
@ -606,7 +625,11 @@ export class PortfolioService {
const userId = await this.getUserId(aImpersonationId, this.request.user.id); const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const orders = ( const orders = (
await this.orderService.getOrders({ userCurrency, userId }) await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
})
).filter(({ SymbolProfile }) => { ).filter(({ SymbolProfile }) => {
return ( return (
SymbolProfile.dataSource === aDataSource && SymbolProfile.dataSource === aDataSource &&
@ -1181,74 +1204,6 @@ export class PortfolioService {
}; };
} }
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
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({ private async getCashPositions({
cashDetails, cashDetails,
emergencyFund, emergencyFund,
@ -1424,14 +1379,117 @@ export class PortfolioService {
return portfolioStart; return portfolioStart;
} }
private async getSummary(
aImpersonationId: string
): Promise<PortfolioSummary> {
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({ private async getTransactionPoints({
filters, filters,
includeDrafts = false, includeDrafts = false,
userId userId,
withExcludedAccounts
}: { }: {
filters?: Filter[]; filters?: Filter[];
includeDrafts?: boolean; includeDrafts?: boolean;
userId: string; userId: string;
withExcludedAccounts?: boolean;
}): Promise<{ }): Promise<{
transactionPoints: TransactionPoint[]; transactionPoints: TransactionPoint[];
orders: OrderWithAccount[]; orders: OrderWithAccount[];
@ -1445,6 +1503,7 @@ export class PortfolioService {
includeDrafts, includeDrafts,
userCurrency, userCurrency,
userId, userId,
withExcludedAccounts,
types: ['BUY', 'SELL'] types: ['BUY', 'SELL']
}); });
@ -1496,17 +1555,22 @@ export class PortfolioService {
orders, orders,
portfolioItemsNow, portfolioItemsNow,
userCurrency, userCurrency,
userId userId,
withExcludedAccounts
}: { }: {
filters?: Filter[]; filters?: Filter[];
orders: OrderWithAccount[]; orders: OrderWithAccount[];
portfolioItemsNow: { [p: string]: TimelinePosition }; portfolioItemsNow: { [p: string]: TimelinePosition };
userCurrency: string; userCurrency: string;
userId: string; userId: string;
withExcludedAccounts?: boolean;
}) { }) {
const accounts: PortfolioDetails['accounts'] = {}; const accounts: PortfolioDetails['accounts'] = {};
let currentAccounts = []; let currentAccounts: (Account & {
Order?: Order[];
Platform?: Platform;
})[] = [];
if (filters.length === 0) { if (filters.length === 0) {
currentAccounts = await this.accountService.getAccounts(userId); 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) { for (const account of currentAccounts) {
const ordersByAccount = orders.filter(({ accountId }) => { const ordersByAccount = orders.filter(({ accountId }) => {
return accountId === account.id; return accountId === account.id;

2
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 }) => { .subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
this.accountType = accountType; this.accountType = accountType;
this.name = name; this.name = name;
this.platformName = Platform?.name; this.platformName = Platform?.name ?? '-';
this.valueInBaseCurrency = valueInBaseCurrency; this.valueInBaseCurrency = valueInBaseCurrency;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();

6
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -21,10 +21,12 @@
<div class="row"> <div class="row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value size="medium" [value]="accountType">Account Type</gf-value> <gf-value i18n size="medium" [value]="accountType"
>Account Type</gf-value
>
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value size="medium" [value]="platformName">Platform</gf-value> <gf-value i18n size="medium" [value]="platformName">Platform</gf-value>
</div> </div>
</div> </div>

50
apps/client/src/app/components/home-summary/home-summary.component.ts

@ -1,8 +1,18 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; 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 { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.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 { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -14,8 +24,11 @@ import { takeUntil } from 'rxjs/operators';
}) })
export class HomeSummaryComponent implements OnDestroy, OnInit { export class HomeSummaryComponent implements OnDestroy, OnInit {
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionForSubscription: boolean;
public hasPermissionToUpdateUserSettings: boolean; public hasPermissionToUpdateUserSettings: boolean;
public info: InfoItem;
public isLoading = true; public isLoading = true;
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
public summary: PortfolioSummary; public summary: PortfolioSummary;
public user: User; public user: User;
@ -25,8 +38,17 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private router: Router,
private snackBar: MatSnackBar,
private userService: UserService private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo();
this.hasPermissionForSubscription = hasPermission(
this.info?.globalPermissions,
permissions.enableSubscription
);
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
@ -50,8 +72,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
.subscribe((aId) => { .subscribe((aId) => {
this.hasImpersonationId = !!aId; this.hasImpersonationId = !!aId;
}); });
this.update();
} }
public onChangeEmergencyFund(emergencyFund: number) { public onChangeEmergencyFund(emergencyFund: number) {
@ -81,12 +101,30 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
this.isLoading = true; this.isLoading = true;
this.dataService this.dataService
.fetchPortfolioSummary() .fetchPortfolioDetails({})
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => { .subscribe(({ summary }) => {
this.summary = response; this.summary = summary;
this.isLoading = false; 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(); this.changeDetectorRef.markForCheck();
}); });

11
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html

@ -172,6 +172,17 @@
></gf-value> ></gf-value>
</div> </div>
</div> </div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Excluded from Analysis</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.excludedAccountsAndActivities"
></gf-value>
</div>
</div>
<div class="row"> <div class="row">
<div class="col"><hr /></div> <div class="col"><hr /></div>
</div> </div>

7
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -59,8 +59,8 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.openCreateAccountDialog(); this.openCreateAccountDialog();
} else if (params['editDialog']) { } else if (params['editDialog']) {
if (this.accounts) { if (this.accounts) {
const account = this.accounts.find((account) => { const account = this.accounts.find(({ id }) => {
return account.id === params['accountId']; return id === params['accountId'];
}); });
this.openUpdateAccountDialog(account); this.openUpdateAccountDialog(account);
@ -155,6 +155,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
balance, balance,
currency, currency,
id, id,
isExcluded,
name, name,
platformId platformId
}: AccountModel): void { }: AccountModel): void {
@ -165,6 +166,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
balance, balance,
currency, currency,
id, id,
isExcluded,
name, name,
platformId platformId
} }
@ -231,6 +233,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
accountType: AccountType.SECURITIES, accountType: AccountType.SECURITIES,
balance: 0, balance: 0,
currency: this.user?.settings?.baseCurrency, currency: this.user?.settings?.baseCurrency,
isExcluded: false,
name: null, name: null,
platformId: null platformId: null
} }

8
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html

@ -50,6 +50,14 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mb-3 px-2">
<mat-checkbox
color="primary"
name="isExcluded"
[(ngModel)]="data.account.isExcluded"
>Exclude from Analysis</mat-checkbox
>
</div>
<div *ngIf="data.account.id"> <div *ngIf="data.account.id">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account ID</mat-label> <mat-label i18n>Account ID</mat-label>

2
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.module.ts

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
@ -15,6 +16,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
CommonModule, CommonModule,
FormsModule, FormsModule,
MatButtonModule, MatButtonModule,
MatCheckboxModule,
MatDialogModule, MatDialogModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,

8
apps/client/src/app/pages/portfolio/fire/fire-page.component.ts

@ -37,14 +37,14 @@ export class FirePageComponent implements OnDestroy, OnInit {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.dataService this.dataService
.fetchPortfolioSummary() .fetchPortfolioDetails({})
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ cash, currentValue }) => { .subscribe(({ summary }) => {
if (cash === null || currentValue === null) { if (summary.cash === null || summary.currentValue === null) {
return; return;
} }
this.fireWealth = new Big(currentValue); this.fireWealth = new Big(summary.currentValue);
this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100); this.withdrawalRatePerYear = this.fireWealth.mul(4).div(100);
this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12); this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12);

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

@ -31,7 +31,6 @@ import {
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioPublicDetails, PortfolioPublicDetails,
PortfolioReport, PortfolioReport,
PortfolioSummary,
UniqueAsset, UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -302,7 +301,11 @@ export class DataService {
); );
} }
public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) { public fetchPortfolioDetails({
filters
}: {
filters?: Filter[];
}): Observable<PortfolioDetails> {
let params = new HttpParams(); let params = new HttpParams();
if (filters?.length > 0) { if (filters?.length > 0) {
@ -348,9 +351,20 @@ export class DataService {
} }
} }
return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', { return this.http
params .get<any>('/api/v1/portfolio/details', {
}); params
})
.pipe(
map((response) => {
if (response.summary?.firstOrderDate) {
response.summary.firstOrderDate = parseISO(
response.summary.firstOrderDate
);
}
return response;
})
);
} }
public fetchPortfolioPerformance({ public fetchPortfolioPerformance({
@ -376,18 +390,6 @@ export class DataService {
return this.http.get<PortfolioReport>('/api/v1/portfolio/report'); return this.http.get<PortfolioReport>('/api/v1/portfolio/report');
} }
public fetchPortfolioSummary(): Observable<PortfolioSummary> {
return this.http.get<any>('/api/v1/portfolio/summary').pipe(
map((summary) => {
if (summary.firstOrderDate) {
summary.firstOrderDate = parseISO(summary.firstOrderDate);
}
return summary;
})
);
}
public fetchPositionDetail({ public fetchPositionDetail({
dataSource, dataSource,
symbol symbol

6
libs/common/src/lib/interfaces/portfolio-details.interface.ts

@ -1,4 +1,7 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import {
PortfolioPosition,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
export interface PortfolioDetails { export interface PortfolioDetails {
accounts: { accounts: {
@ -13,5 +16,6 @@ export interface PortfolioDetails {
filteredValueInBaseCurrency?: number; filteredValueInBaseCurrency?: number;
filteredValueInPercentage: number; filteredValueInPercentage: number;
holdings: { [symbol: string]: PortfolioPosition }; holdings: { [symbol: string]: PortfolioPosition };
summary: PortfolioSummary;
totalValueInBaseCurrency?: number; totalValueInBaseCurrency?: number;
} }

3
libs/common/src/lib/interfaces/portfolio-summary.interface.ts

@ -3,9 +3,10 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
export interface PortfolioSummary extends PortfolioPerformance { export interface PortfolioSummary extends PortfolioPerformance {
annualizedPerformancePercent: number; annualizedPerformancePercent: number;
cash: number; cash: number;
dividend: number;
committedFunds: number; committedFunds: number;
dividend: number;
emergencyFund: number; emergencyFund: number;
excludedAccountsAndActivities: number;
fees: number; fees: number;
firstOrderDate: Date; firstOrderDate: Date;
items: number; items: number;

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "1.197.0", "version": "1.198.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {

2
prisma/migrations/20210605161257_added_symbol_profile/migration.sql

@ -1,5 +1,5 @@
-- AlterTable -- AlterTable
ALTER TABLE "Order" ADD COLUMN "symbolProfileId" TEXT; ALTER TABLE "Order" ADD COLUMN "symbolProfileId" TEXT;
-- CreateTable -- CreateTable
CREATE TABLE "SymbolProfile" ( CREATE TABLE "SymbolProfile" (

2
prisma/migrations/20210612110542_added_auth_device/migration.sql

@ -1,5 +1,5 @@
-- AlterTable -- AlterTable
ALTER TABLE "User" ADD COLUMN "authChallenge" TEXT; ALTER TABLE "User" ADD COLUMN "authChallenge" TEXT;
-- CreateTable -- CreateTable
CREATE TABLE "AuthDevice" ( CREATE TABLE "AuthDevice" (

2
prisma/migrations/20210616075245_added_sectors_to_symbol_profile/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "sectors" JSONB; ALTER TABLE "SymbolProfile" ADD COLUMN "sectors" JSONB;

4
prisma/migrations/20210703194509_added_balance_to_account/migration.sql

@ -2,5 +2,5 @@
ALTER TYPE "AccountType" ADD VALUE 'CASH'; ALTER TYPE "AccountType" ADD VALUE 'CASH';
-- AlterTable -- AlterTable
ALTER TABLE "Account" ADD COLUMN "balance" DOUBLE PRECISION NOT NULL DEFAULT 0, ALTER TABLE "Account" ADD COLUMN "balance" DOUBLE PRECISION NOT NULL DEFAULT 0,
ADD COLUMN "currency" "Currency" NOT NULL DEFAULT E'USD'; ADD COLUMN "currency" "Currency" NOT NULL DEFAULT E'USD';

2
prisma/migrations/20210724160404_added_currency_to_symbol_profile/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "currency" "Currency"; ALTER TABLE "SymbolProfile" ADD COLUMN "currency" "Currency";

2
prisma/migrations/20210807062952_added_is_draft_to_order/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false; ALTER TABLE "Order" ADD COLUMN "isDraft" BOOLEAN NOT NULL DEFAULT false;

2
prisma/migrations/20210808075949_added_asset_class_to_symbol_profile/migration.sql

@ -2,4 +2,4 @@
CREATE TYPE "AssetClass" AS ENUM ('CASH', 'COMMODITY', 'EQUITY'); CREATE TYPE "AssetClass" AS ENUM ('CASH', 'COMMODITY', 'EQUITY');
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "assetClass" "AssetClass"; ALTER TABLE "SymbolProfile" ADD COLUMN "assetClass" "AssetClass";

2
prisma/migrations/20210815180121_added_settings_to_settings/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "Settings" ADD COLUMN "settings" JSONB; ALTER TABLE "Settings" ADD COLUMN "settings" JSONB;

2
prisma/migrations/20210822200534_added_asset_sub_class_to_symbol_profile/migration.sql

@ -2,4 +2,4 @@
CREATE TYPE "AssetSubClass" AS ENUM ('CRYPTOCURRENCY', 'ETF', 'STOCK'); CREATE TYPE "AssetSubClass" AS ENUM ('CRYPTOCURRENCY', 'ETF', 'STOCK');
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "assetSubClass" "AssetSubClass"; ALTER TABLE "SymbolProfile" ADD COLUMN "assetSubClass" "AssetSubClass";

2
prisma/migrations/20210916182355_added_data_source_to_market_data/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- 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';

2
prisma/migrations/20211107082008_added_symbol_mapping_to_symbol_profile/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "symbolMapping" JSONB; ALTER TABLE "SymbolProfile" ADD COLUMN "symbolMapping" JSONB;

2
prisma/migrations/20211107171624_added_scraper_configuration_to_symbol_profile/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "scraperConfiguration" JSONB; ALTER TABLE "SymbolProfile" ADD COLUMN "scraperConfiguration" JSONB;

2
prisma/migrations/20220227093650_added_url_to_symbol_profile/migration.sql

@ -1,2 +1,2 @@
-- AlterTable -- AlterTable
ALTER TABLE "SymbolProfile" ADD COLUMN "url" TEXT; ALTER TABLE "SymbolProfile" ADD COLUMN "url" TEXT;

2
prisma/migrations/20220924175215_added_is_excluded_to_account/migration.sql

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "isExcluded" BOOLEAN NOT NULL DEFAULT false;

1
prisma/schema.prisma

@ -27,6 +27,7 @@ model Account {
currency String? currency String?
id String @default(uuid()) id String @default(uuid())
isDefault Boolean @default(false) isDefault Boolean @default(false)
isExcluded Boolean @default(false)
name String? name String?
platformId String? platformId String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

Loading…
Cancel
Save