Feature/add support for emergency fund (#749)
* Add support for emergency fund * Update changelog
This commit is contained in:
parent
07799573cb
commit
7b6893b5ed
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added support for an emergency fund
|
||||||
- Added the contexts to the logger commands
|
- Added the contexts to the logger commands
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -334,6 +334,7 @@ export class PortfolioController {
|
|||||||
'currentNetPerformance',
|
'currentNetPerformance',
|
||||||
'currentValue',
|
'currentValue',
|
||||||
'dividend',
|
'dividend',
|
||||||
|
'emergencyFund',
|
||||||
'fees',
|
'fees',
|
||||||
'items',
|
'items',
|
||||||
'netWorth',
|
'netWorth',
|
||||||
|
@ -5,6 +5,8 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s
|
|||||||
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
|
||||||
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
|
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
|
||||||
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
||||||
|
import { UserSettings } from '@ghostfolio/api/app/user/interfaces/user-settings.interface';
|
||||||
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
||||||
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
|
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
|
||||||
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
|
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
|
||||||
@ -19,7 +21,11 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation.ser
|
|||||||
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
|
import {
|
||||||
|
ASSET_SUB_CLASS_EMERGENCY_FUND,
|
||||||
|
UNKNOWN_KEY,
|
||||||
|
baseCurrency
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
Accounts,
|
Accounts,
|
||||||
@ -76,7 +82,8 @@ export class PortfolioServiceNew {
|
|||||||
private readonly orderService: OrderService,
|
private readonly orderService: OrderService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
private readonly rulesService: RulesService,
|
private readonly rulesService: RulesService,
|
||||||
private readonly symbolProfileService: SymbolProfileService
|
private readonly symbolProfileService: SymbolProfileService,
|
||||||
|
private readonly userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getAccounts(aUserId: string): Promise<AccountWithValue[]> {
|
public async getAccounts(aUserId: string): Promise<AccountWithValue[]> {
|
||||||
@ -295,7 +302,11 @@ export class PortfolioServiceNew {
|
|||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
): 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 emergencyFund = new Big(
|
||||||
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
|
);
|
||||||
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
||||||
|
|
||||||
const { orders, portfolioOrders, transactionPoints } =
|
const { orders, portfolioOrders, transactionPoints } =
|
||||||
@ -393,6 +404,7 @@ export class PortfolioServiceNew {
|
|||||||
|
|
||||||
const cashPositions = await this.getCashPositions({
|
const cashPositions = await this.getCashPositions({
|
||||||
cashDetails,
|
cashDetails,
|
||||||
|
emergencyFund,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
investment: totalInvestment,
|
investment: totalInvestment,
|
||||||
value: totalValue
|
value: totalValue
|
||||||
@ -883,6 +895,7 @@ export class PortfolioServiceNew {
|
|||||||
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
|
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
|
||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
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 performanceInformation = await this.getPerformance(aImpersonationId);
|
||||||
|
|
||||||
@ -895,6 +908,9 @@ export class PortfolioServiceNew {
|
|||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
const dividend = this.getDividend(orders).toNumber();
|
const dividend = this.getDividend(orders).toNumber();
|
||||||
|
const emergencyFund = new Big(
|
||||||
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
|
);
|
||||||
const fees = this.getFees(orders).toNumber();
|
const fees = this.getFees(orders).toNumber();
|
||||||
const firstOrderDate = orders[0]?.date;
|
const firstOrderDate = orders[0]?.date;
|
||||||
const items = this.getItems(orders).toNumber();
|
const items = this.getItems(orders).toNumber();
|
||||||
@ -902,6 +918,7 @@ export class PortfolioServiceNew {
|
|||||||
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
|
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
|
||||||
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
|
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
|
||||||
|
|
||||||
|
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
|
||||||
const committedFunds = new Big(totalBuy).minus(totalSell);
|
const committedFunds = new Big(totalBuy).minus(totalSell);
|
||||||
|
|
||||||
const netWorth = new Big(balanceInBaseCurrency)
|
const netWorth = new Big(balanceInBaseCurrency)
|
||||||
@ -927,6 +944,7 @@ export class PortfolioServiceNew {
|
|||||||
return {
|
return {
|
||||||
...performanceInformation.performance,
|
...performanceInformation.performance,
|
||||||
annualizedPerformancePercent,
|
annualizedPerformancePercent,
|
||||||
|
cash,
|
||||||
dividend,
|
dividend,
|
||||||
fees,
|
fees,
|
||||||
firstOrderDate,
|
firstOrderDate,
|
||||||
@ -934,8 +952,8 @@ export class PortfolioServiceNew {
|
|||||||
netWorth,
|
netWorth,
|
||||||
totalBuy,
|
totalBuy,
|
||||||
totalSell,
|
totalSell,
|
||||||
cash: balanceInBaseCurrency,
|
|
||||||
committedFunds: committedFunds.toNumber(),
|
committedFunds: committedFunds.toNumber(),
|
||||||
|
emergencyFund: emergencyFund.toNumber(),
|
||||||
ordersCount: orders.filter((order) => {
|
ordersCount: orders.filter((order) => {
|
||||||
return order.type === 'BUY' || order.type === 'SELL';
|
return order.type === 'BUY' || order.type === 'SELL';
|
||||||
}).length
|
}).length
|
||||||
@ -944,16 +962,18 @@ export class PortfolioServiceNew {
|
|||||||
|
|
||||||
private async getCashPositions({
|
private async getCashPositions({
|
||||||
cashDetails,
|
cashDetails,
|
||||||
|
emergencyFund,
|
||||||
investment,
|
investment,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
value
|
value
|
||||||
}: {
|
}: {
|
||||||
cashDetails: CashDetails;
|
cashDetails: CashDetails;
|
||||||
|
emergencyFund: Big;
|
||||||
investment: Big;
|
investment: Big;
|
||||||
value: Big;
|
value: Big;
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
}) {
|
}) {
|
||||||
const cashPositions = {};
|
const cashPositions: PortfolioDetails['holdings'] = {};
|
||||||
|
|
||||||
for (const account of cashDetails.accounts) {
|
for (const account of cashDetails.accounts) {
|
||||||
const convertedBalance = this.exchangeRateDataService.toCurrency(
|
const convertedBalance = this.exchangeRateDataService.toCurrency(
|
||||||
@ -977,6 +997,7 @@ export class PortfolioServiceNew {
|
|||||||
assetSubClass: AssetClass.CASH,
|
assetSubClass: AssetClass.CASH,
|
||||||
countries: [],
|
countries: [],
|
||||||
currency: account.currency,
|
currency: account.currency,
|
||||||
|
dataSource: undefined,
|
||||||
grossPerformance: 0,
|
grossPerformance: 0,
|
||||||
grossPerformancePercent: 0,
|
grossPerformancePercent: 0,
|
||||||
investment: convertedBalance,
|
investment: convertedBalance,
|
||||||
@ -994,6 +1015,28 @@ export class PortfolioServiceNew {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (emergencyFund.gt(0)) {
|
||||||
|
cashPositions[ASSET_SUB_CLASS_EMERGENCY_FUND] = {
|
||||||
|
...cashPositions[userCurrency],
|
||||||
|
assetSubClass: ASSET_SUB_CLASS_EMERGENCY_FUND,
|
||||||
|
investment: emergencyFund.toNumber(),
|
||||||
|
name: ASSET_SUB_CLASS_EMERGENCY_FUND,
|
||||||
|
symbol: ASSET_SUB_CLASS_EMERGENCY_FUND,
|
||||||
|
value: emergencyFund.toNumber()
|
||||||
|
};
|
||||||
|
|
||||||
|
cashPositions[userCurrency].investment = new Big(
|
||||||
|
cashPositions[userCurrency].investment
|
||||||
|
)
|
||||||
|
.minus(emergencyFund)
|
||||||
|
.toNumber();
|
||||||
|
cashPositions[userCurrency].value = new Big(
|
||||||
|
cashPositions[userCurrency].value
|
||||||
|
)
|
||||||
|
.minus(emergencyFund)
|
||||||
|
.toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
for (const symbol of Object.keys(cashPositions)) {
|
for (const symbol of Object.keys(cashPositions)) {
|
||||||
// Calculate allocations for each currency
|
// Calculate allocations for each currency
|
||||||
cashPositions[symbol].allocationCurrent = new Big(
|
cashPositions[symbol].allocationCurrent = new Big(
|
||||||
|
@ -6,6 +6,8 @@ import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfol
|
|||||||
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
|
import { TimelineSpecification } from '@ghostfolio/api/app/portfolio/interfaces/timeline-specification.interface';
|
||||||
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
|
||||||
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/portfolio-calculator';
|
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/portfolio-calculator';
|
||||||
|
import { UserSettings } from '@ghostfolio/api/app/user/interfaces/user-settings.interface';
|
||||||
|
import { UserService } from '@ghostfolio/api/app/user/user.service';
|
||||||
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
|
||||||
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
|
import { AccountClusterRiskInitialInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/initial-investment';
|
||||||
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
|
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
|
||||||
@ -20,7 +22,11 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation.ser
|
|||||||
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
|
import {
|
||||||
|
ASSET_SUB_CLASS_EMERGENCY_FUND,
|
||||||
|
UNKNOWN_KEY,
|
||||||
|
baseCurrency
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||||
import {
|
import {
|
||||||
Accounts,
|
Accounts,
|
||||||
@ -75,7 +81,8 @@ export class PortfolioService {
|
|||||||
private readonly orderService: OrderService,
|
private readonly orderService: OrderService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser,
|
@Inject(REQUEST) private readonly request: RequestWithUser,
|
||||||
private readonly rulesService: RulesService,
|
private readonly rulesService: RulesService,
|
||||||
private readonly symbolProfileService: SymbolProfileService
|
private readonly symbolProfileService: SymbolProfileService,
|
||||||
|
private readonly userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async getAccounts(aUserId: string): Promise<AccountWithValue[]> {
|
public async getAccounts(aUserId: string): Promise<AccountWithValue[]> {
|
||||||
@ -286,7 +293,11 @@ export class PortfolioService {
|
|||||||
aDateRange: DateRange = 'max'
|
aDateRange: DateRange = 'max'
|
||||||
): 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 emergencyFund = new Big(
|
||||||
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
|
);
|
||||||
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
this.currentRateService,
|
this.currentRateService,
|
||||||
@ -381,6 +392,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const cashPositions = await this.getCashPositions({
|
const cashPositions = await this.getCashPositions({
|
||||||
cashDetails,
|
cashDetails,
|
||||||
|
emergencyFund,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
investment: totalInvestment,
|
investment: totalInvestment,
|
||||||
value: totalValue
|
value: totalValue
|
||||||
@ -861,6 +873,7 @@ export class PortfolioService {
|
|||||||
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
|
public async getSummary(aImpersonationId: string): Promise<PortfolioSummary> {
|
||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
|
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 performanceInformation = await this.getPerformance(aImpersonationId);
|
||||||
|
|
||||||
@ -873,6 +886,9 @@ export class PortfolioService {
|
|||||||
userId
|
userId
|
||||||
});
|
});
|
||||||
const dividend = this.getDividend(orders).toNumber();
|
const dividend = this.getDividend(orders).toNumber();
|
||||||
|
const emergencyFund = new Big(
|
||||||
|
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
|
||||||
|
);
|
||||||
const fees = this.getFees(orders).toNumber();
|
const fees = this.getFees(orders).toNumber();
|
||||||
const firstOrderDate = orders[0]?.date;
|
const firstOrderDate = orders[0]?.date;
|
||||||
const items = this.getItems(orders).toNumber();
|
const items = this.getItems(orders).toNumber();
|
||||||
@ -880,6 +896,7 @@ export class PortfolioService {
|
|||||||
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
|
const totalBuy = this.getTotalByType(orders, userCurrency, 'BUY');
|
||||||
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
|
const totalSell = this.getTotalByType(orders, userCurrency, 'SELL');
|
||||||
|
|
||||||
|
const cash = new Big(balanceInBaseCurrency).minus(emergencyFund).toNumber();
|
||||||
const committedFunds = new Big(totalBuy).minus(totalSell);
|
const committedFunds = new Big(totalBuy).minus(totalSell);
|
||||||
|
|
||||||
const netWorth = new Big(balanceInBaseCurrency)
|
const netWorth = new Big(balanceInBaseCurrency)
|
||||||
@ -889,6 +906,7 @@ export class PortfolioService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...performanceInformation.performance,
|
...performanceInformation.performance,
|
||||||
|
cash,
|
||||||
dividend,
|
dividend,
|
||||||
fees,
|
fees,
|
||||||
firstOrderDate,
|
firstOrderDate,
|
||||||
@ -898,8 +916,8 @@ export class PortfolioService {
|
|||||||
totalSell,
|
totalSell,
|
||||||
annualizedPerformancePercent:
|
annualizedPerformancePercent:
|
||||||
performanceInformation.performance.annualizedPerformancePercent,
|
performanceInformation.performance.annualizedPerformancePercent,
|
||||||
cash: balanceInBaseCurrency,
|
|
||||||
committedFunds: committedFunds.toNumber(),
|
committedFunds: committedFunds.toNumber(),
|
||||||
|
emergencyFund: emergencyFund.toNumber(),
|
||||||
ordersCount: orders.filter((order) => {
|
ordersCount: orders.filter((order) => {
|
||||||
return order.type === 'BUY' || order.type === 'SELL';
|
return order.type === 'BUY' || order.type === 'SELL';
|
||||||
}).length
|
}).length
|
||||||
@ -908,16 +926,18 @@ export class PortfolioService {
|
|||||||
|
|
||||||
private async getCashPositions({
|
private async getCashPositions({
|
||||||
cashDetails,
|
cashDetails,
|
||||||
|
emergencyFund,
|
||||||
investment,
|
investment,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
value
|
value
|
||||||
}: {
|
}: {
|
||||||
cashDetails: CashDetails;
|
cashDetails: CashDetails;
|
||||||
|
emergencyFund: Big;
|
||||||
investment: Big;
|
investment: Big;
|
||||||
userCurrency: string;
|
userCurrency: string;
|
||||||
value: Big;
|
value: Big;
|
||||||
}) {
|
}) {
|
||||||
const cashPositions = {};
|
const cashPositions: PortfolioDetails['holdings'] = {};
|
||||||
|
|
||||||
for (const account of cashDetails.accounts) {
|
for (const account of cashDetails.accounts) {
|
||||||
const convertedBalance = this.exchangeRateDataService.toCurrency(
|
const convertedBalance = this.exchangeRateDataService.toCurrency(
|
||||||
@ -941,6 +961,7 @@ export class PortfolioService {
|
|||||||
assetSubClass: AssetClass.CASH,
|
assetSubClass: AssetClass.CASH,
|
||||||
countries: [],
|
countries: [],
|
||||||
currency: account.currency,
|
currency: account.currency,
|
||||||
|
dataSource: undefined,
|
||||||
grossPerformance: 0,
|
grossPerformance: 0,
|
||||||
grossPerformancePercent: 0,
|
grossPerformancePercent: 0,
|
||||||
investment: convertedBalance,
|
investment: convertedBalance,
|
||||||
@ -958,6 +979,28 @@ export class PortfolioService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (emergencyFund.gt(0)) {
|
||||||
|
cashPositions[ASSET_SUB_CLASS_EMERGENCY_FUND] = {
|
||||||
|
...cashPositions[userCurrency],
|
||||||
|
assetSubClass: ASSET_SUB_CLASS_EMERGENCY_FUND,
|
||||||
|
investment: emergencyFund.toNumber(),
|
||||||
|
name: ASSET_SUB_CLASS_EMERGENCY_FUND,
|
||||||
|
symbol: ASSET_SUB_CLASS_EMERGENCY_FUND,
|
||||||
|
value: emergencyFund.toNumber()
|
||||||
|
};
|
||||||
|
|
||||||
|
cashPositions[userCurrency].investment = new Big(
|
||||||
|
cashPositions[userCurrency].investment
|
||||||
|
)
|
||||||
|
.minus(emergencyFund)
|
||||||
|
.toNumber();
|
||||||
|
cashPositions[userCurrency].value = new Big(
|
||||||
|
cashPositions[userCurrency].value
|
||||||
|
)
|
||||||
|
.minus(emergencyFund)
|
||||||
|
.toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
for (const symbol of Object.keys(cashPositions)) {
|
for (const symbol of Object.keys(cashPositions)) {
|
||||||
// Calculate allocations for each currency
|
// Calculate allocations for each currency
|
||||||
cashPositions[symbol].allocationCurrent = new Big(
|
cashPositions[symbol].allocationCurrent = new Big(
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
export interface UserSettings {
|
export interface UserSettings {
|
||||||
|
emergencyFund?: number;
|
||||||
|
isNewCalculationEngine?: boolean;
|
||||||
isRestrictedView?: boolean;
|
isRestrictedView?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { IsBoolean, IsOptional } from 'class-validator';
|
import { IsBoolean, IsNumber, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
export class UpdateUserSettingDto {
|
export class UpdateUserSettingDto {
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
emergencyFund?: number;
|
||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isNewCalculationEngine?: boolean;
|
isNewCalculationEngine?: boolean;
|
||||||
|
@ -23,7 +23,6 @@ import {
|
|||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { Provider, Role } from '@prisma/client';
|
|
||||||
import { User as UserModel } from '@prisma/client';
|
import { User as UserModel } from '@prisma/client';
|
||||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Prisma, Provider, Role, User, ViewMode } from '@prisma/client';
|
import { Prisma, Role, User, ViewMode } from '@prisma/client';
|
||||||
|
|
||||||
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
import { UserSettingsParams } from './interfaces/user-settings-params.interface';
|
||||||
import { UserSettings } from './interfaces/user-settings.interface';
|
import { UserSettings } from './interfaces/user-settings.interface';
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
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 { 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 { PortfolioSummary, User } from '@ghostfolio/common/interfaces';
|
||||||
|
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';
|
||||||
|
|
||||||
@ -11,6 +13,8 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './home-summary.html'
|
templateUrl: './home-summary.html'
|
||||||
})
|
})
|
||||||
export class HomeSummaryComponent implements OnDestroy, OnInit {
|
export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||||
|
public hasImpersonationId: boolean;
|
||||||
|
public hasPermissionToUpdateUserSettings: boolean;
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public summary: PortfolioSummary;
|
public summary: PortfolioSummary;
|
||||||
public user: User;
|
public user: User;
|
||||||
@ -23,6 +27,7 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
@ -31,6 +36,11 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.hasPermissionToUpdateUserSettings = hasPermission(
|
||||||
|
this.user.permissions,
|
||||||
|
permissions.updateUserSettings
|
||||||
|
);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -40,9 +50,25 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
|
|||||||
* Initializes the controller
|
* Initializes the controller
|
||||||
*/
|
*/
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
|
this.impersonationStorageService
|
||||||
|
.onChangeHasImpersonation()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((aId) => {
|
||||||
|
this.hasImpersonationId = !!aId;
|
||||||
|
});
|
||||||
|
|
||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onChangeEmergencyFund(emergencyFund: number) {
|
||||||
|
this.dataService
|
||||||
|
.putUserSetting({ emergencyFund })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
|
@ -8,9 +8,11 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-summary
|
<gf-portfolio-summary
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[hasPermissionToUpdateUserSettings]="!hasImpersonationId && hasPermissionToUpdateUserSettings"
|
||||||
[isLoading]="isLoading"
|
[isLoading]="isLoading"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[summary]="summary"
|
[summary]="summary"
|
||||||
|
(emergencyFundChanged)="onChangeEmergencyFund($event)"
|
||||||
></gf-portfolio-summary>
|
></gf-portfolio-summary>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -130,6 +130,26 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Emergency Fund</div>
|
||||||
|
<div
|
||||||
|
class="align-items-center d-flex justify-content-end"
|
||||||
|
[ngClass]="{ 'cursor-pointer': hasPermissionToUpdateUserSettings }"
|
||||||
|
(click)="hasPermissionToUpdateUserSettings && onEditEmergencyFund()"
|
||||||
|
>
|
||||||
|
<ion-icon
|
||||||
|
*ngIf="hasPermissionToUpdateUserSettings && !isLoading"
|
||||||
|
class="mr-1 text-muted"
|
||||||
|
name="ellipsis-horizontal-circle-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.emergencyFund"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row px-3 py-1">
|
<div class="row px-3 py-1">
|
||||||
<div class="d-flex flex-grow-1" i18n>Cash (Buying Power)</div>
|
<div class="d-flex flex-grow-1" i18n>Cash (Buying Power)</div>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
OnInit
|
OnInit,
|
||||||
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
|
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
@ -16,10 +18,13 @@ import { formatDistanceToNow } from 'date-fns';
|
|||||||
})
|
})
|
||||||
export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
|
@Input() hasPermissionToUpdateUserSettings: boolean;
|
||||||
@Input() isLoading: boolean;
|
@Input() isLoading: boolean;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() summary: PortfolioSummary;
|
@Input() summary: PortfolioSummary;
|
||||||
|
|
||||||
|
@Output() emergencyFundChanged = new EventEmitter<number>();
|
||||||
|
|
||||||
public timeInMarket: string;
|
public timeInMarket: string;
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
@ -37,4 +42,16 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
|
|||||||
this.timeInMarket = undefined;
|
this.timeInMarket = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onEditEmergencyFund() {
|
||||||
|
const emergencyFundInput = prompt(
|
||||||
|
'Please enter the amount of your emergency fund:',
|
||||||
|
this.summary.emergencyFund.toString()
|
||||||
|
);
|
||||||
|
const emergencyFund = parseFloat(emergencyFundInput?.trim());
|
||||||
|
|
||||||
|
if (emergencyFund >= 0) {
|
||||||
|
this.emergencyFundChanged.emit(emergencyFund);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
||||||
@ -8,6 +8,6 @@ import { PortfolioSummaryComponent } from './portfolio-summary.component';
|
|||||||
declarations: [PortfolioSummaryComponent],
|
declarations: [PortfolioSummaryComponent],
|
||||||
exports: [PortfolioSummaryComponent],
|
exports: [PortfolioSummaryComponent],
|
||||||
imports: [CommonModule, GfValueModule],
|
imports: [CommonModule, GfValueModule],
|
||||||
providers: []
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfPortfolioSummaryModule {}
|
export class GfPortfolioSummaryModule {}
|
||||||
|
@ -13,6 +13,7 @@ import { MatPaginator } from '@angular/material/paginator';
|
|||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
import { ASSET_SUB_CLASS_EMERGENCY_FUND } from '@ghostfolio/common/config';
|
||||||
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
|
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { AssetClass, Order as OrderModel } from '@prisma/client';
|
import { AssetClass, Order as OrderModel } from '@prisma/client';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
@ -39,7 +40,10 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
public dataSource: MatTableDataSource<PortfolioPosition> =
|
public dataSource: MatTableDataSource<PortfolioPosition> =
|
||||||
new MatTableDataSource();
|
new MatTableDataSource();
|
||||||
public displayedColumns = [];
|
public displayedColumns = [];
|
||||||
public ignoreAssetSubClasses = [AssetClass.CASH.toString()];
|
public ignoreAssetSubClasses = [
|
||||||
|
AssetClass.CASH.toString(),
|
||||||
|
ASSET_SUB_CLASS_EMERGENCY_FUND
|
||||||
|
];
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
public pageSize = 7;
|
public pageSize = 7;
|
||||||
public routeQueryParams: Subscription;
|
public routeQueryParams: Subscription;
|
||||||
|
@ -42,6 +42,8 @@ export const warnColorRgb = {
|
|||||||
b: 69
|
b: 69
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ASSET_SUB_CLASS_EMERGENCY_FUND = 'EMERGENCY_FUND';
|
||||||
|
|
||||||
export const DEFAULT_DATE_FORMAT = 'dd.MM.yyyy';
|
export const DEFAULT_DATE_FORMAT = 'dd.MM.yyyy';
|
||||||
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
|
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ export interface PortfolioPosition {
|
|||||||
allocationCurrent: number;
|
allocationCurrent: number;
|
||||||
allocationInvestment: number;
|
allocationInvestment: number;
|
||||||
assetClass?: AssetClass;
|
assetClass?: AssetClass;
|
||||||
assetSubClass?: AssetSubClass | 'CASH';
|
assetSubClass?: AssetSubClass | 'CASH' | 'EMERGENCY_FUND';
|
||||||
countries: Country[];
|
countries: Country[];
|
||||||
currency: string;
|
currency: string;
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
|
@ -5,6 +5,7 @@ export interface PortfolioSummary extends PortfolioPerformance {
|
|||||||
cash: number;
|
cash: number;
|
||||||
dividend: number;
|
dividend: number;
|
||||||
committedFunds: number;
|
committedFunds: number;
|
||||||
|
emergencyFund: number;
|
||||||
fees: number;
|
fees: number;
|
||||||
firstOrderDate: Date;
|
firstOrderDate: Date;
|
||||||
items: number;
|
items: number;
|
||||||
|
@ -122,7 +122,7 @@ export class PortfolioProportionChartComponent
|
|||||||
chartData[this.positions[symbol][this.keys[0]]] = {
|
chartData[this.positions[symbol][this.keys[0]]] = {
|
||||||
name: this.positions[symbol].name,
|
name: this.positions[symbol].name,
|
||||||
subCategory: {},
|
subCategory: {},
|
||||||
value: new Big(this.positions[symbol].value)
|
value: new Big(this.positions[symbol].value ?? 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.positions[symbol][this.keys[1]]) {
|
if (this.positions[symbol][this.keys[1]]) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user