Browse Source

Add emergency fund coverage rule

pull/7538/head
Thomas Kaul 3 weeks ago
parent
commit
0afd1be7f0
  1. 26
      apps/api/src/app/portfolio/portfolio.service.ts
  2. 74
      apps/api/src/models/rules/emergency-fund/emergency-fund-coverage.ts
  3. 3
      apps/client/src/app/pages/i18n/i18n-page.html

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

@ -1136,17 +1136,22 @@ export class PortfolioService {
withSummary: true withSummary: true
}); });
const { balanceInBaseCurrency: cashBalanceInBaseCurrency } = // The cash balance of the summary is split into the emergency fund and
await this.accountService.getCashDetails({ // the remainder, both denominated in the base currency of the user
userId, const cashBalanceInBaseCurrency = new Big(summary.cash)
currency: this.getUserCurrency() .plus(summary.emergencyFund.cash)
}); .toNumber();
const emergencyFundInBaseCurrency = userSettings.emergencyFund ?? 0; const emergencyFundInBaseCurrency = userSettings.emergencyFund ?? 0;
const emergencyFundHoldingsValueInBaseCurrency = const emergencyFundHoldingsValueInBaseCurrency =
this.getEmergencyFundHoldingsValueInBaseCurrency({ holdings }); this.getEmergencyFundHoldingsValueInBaseCurrency({ holdings });
const totalEmergencyFundInBaseCurrency = this.getTotalEmergencyFund({
emergencyFundHoldingsValueInBaseCurrency,
userSettings
}).toNumber();
const hasOpenHoldings = Object.keys(holdings).length > 0; const hasOpenHoldings = Object.keys(holdings).length > 0;
const marketsAdvancedTotalInBaseCurrency = getSum( const marketsAdvancedTotalInBaseCurrency = getSum(
@ -1192,14 +1197,11 @@ export class PortfolioService {
this.exchangeRateDataService, this.exchangeRateDataService,
this.i18nService, this.i18nService,
userSettings.language, userSettings.language,
this.getTotalEmergencyFund({ totalEmergencyFundInBaseCurrency
emergencyFundHoldingsValueInBaseCurrency,
userSettings
}).toNumber()
), ),
// The coverage is only meaningful once a target has been set, // The coverage is only meaningful once an emergency fund has been
// otherwise the set up rule already reports the missing setup // set up, either by an amount or by the tagged holdings
...(emergencyFundInBaseCurrency > 0 ...(totalEmergencyFundInBaseCurrency > 0
? [ ? [
new EmergencyFundCoverage( new EmergencyFundCoverage(
this.exchangeRateDataService, this.exchangeRateDataService,

74
apps/api/src/models/rules/emergency-fund/emergency-fund-coverage.ts

@ -4,67 +4,68 @@ import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
import { DEFAULT_CURRENCY, DEFAULT_LOCALE } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY, DEFAULT_LOCALE } from '@ghostfolio/common/config';
import { RuleSettings, UserSettings } from '@ghostfolio/common/interfaces'; import { RuleSettings, UserSettings } from '@ghostfolio/common/interfaces';
export class EmergencyFundCoverage extends Rule<Settings> { import { Big } from 'big.js';
private cashBalanceInBaseCurrency: number;
private emergencyFundHoldingsValueInBaseCurrency: number;
private emergencyFundInBaseCurrency: number;
export class EmergencyFundCoverage extends Rule<Settings> {
public constructor( public constructor(
exchangeRateDataService: ExchangeRateDataService, exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService, private i18nService: I18nService,
languageCode: string, languageCode: string,
emergencyFundInBaseCurrency: number, private emergencyFundInBaseCurrency: number,
emergencyFundHoldingsValueInBaseCurrency: number, private emergencyFundHoldingsValueInBaseCurrency: number,
cashBalanceInBaseCurrency: number private cashBalanceInBaseCurrency: number
) { ) {
super(exchangeRateDataService, { super(exchangeRateDataService, {
languageCode, languageCode,
key: EmergencyFundCoverage.name key: EmergencyFundCoverage.name
}); });
this.cashBalanceInBaseCurrency = cashBalanceInBaseCurrency;
this.emergencyFundHoldingsValueInBaseCurrency =
emergencyFundHoldingsValueInBaseCurrency;
this.emergencyFundInBaseCurrency = emergencyFundInBaseCurrency;
} }
public evaluate(ruleSettings: Settings) { public evaluate(ruleSettings: Settings) {
if (!this.emergencyFundInBaseCurrency) {
return {
evaluation: this.i18nService.getTranslation({
id: 'rule.emergencyFundCoverage.false.unset',
languageCode: this.getLanguageCode()
}),
value: false
};
}
const placeholders = {
baseCurrency: ruleSettings.baseCurrency,
emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString(
ruleSettings.locale
)
};
// Only the holdings tagged as emergency fund are an explicit commitment, // Only the holdings tagged as emergency fund are an explicit commitment,
// the cash balance covers the remainder // the cash balance covers the remainder
if ( if (
this.emergencyFundHoldingsValueInBaseCurrency > new Big(this.emergencyFundHoldingsValueInBaseCurrency).gt(
this.emergencyFundInBaseCurrency this.emergencyFundInBaseCurrency
)
) { ) {
return { return {
evaluation: this.i18nService.getTranslation({ evaluation: this.i18nService.getTranslation({
placeholders,
id: 'rule.emergencyFundCoverage.false.over', id: 'rule.emergencyFundCoverage.false.over',
languageCode: this.getLanguageCode(), languageCode: this.getLanguageCode()
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString(
ruleSettings.locale
)
}
}), }),
value: false value: false
}; };
} }
const coverageInBaseCurrency = const coverageInBaseCurrency = new Big(
this.emergencyFundHoldingsValueInBaseCurrency + this.emergencyFundHoldingsValueInBaseCurrency
this.cashBalanceInBaseCurrency; ).plus(this.cashBalanceInBaseCurrency);
if (coverageInBaseCurrency < this.emergencyFundInBaseCurrency) { if (coverageInBaseCurrency.lt(this.emergencyFundInBaseCurrency)) {
return { return {
evaluation: this.i18nService.getTranslation({ evaluation: this.i18nService.getTranslation({
placeholders,
id: 'rule.emergencyFundCoverage.false.under', id: 'rule.emergencyFundCoverage.false.under',
languageCode: this.getLanguageCode(), languageCode: this.getLanguageCode()
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString(
ruleSettings.locale
)
}
}), }),
value: false value: false
}; };
@ -72,14 +73,9 @@ export class EmergencyFundCoverage extends Rule<Settings> {
return { return {
evaluation: this.i18nService.getTranslation({ evaluation: this.i18nService.getTranslation({
placeholders,
id: 'rule.emergencyFundCoverage.true', id: 'rule.emergencyFundCoverage.true',
languageCode: this.getLanguageCode(), languageCode: this.getLanguageCode()
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString(
ruleSettings.locale
)
}
}), }),
value: true value: true
}; };

3
apps/client/src/app/pages/i18n/i18n-page.html

@ -151,6 +151,9 @@
Your emergency fund of $&#123;emergencyFund&#125; Your emergency fund of $&#123;emergencyFund&#125;
$&#123;baseCurrency&#125; is not covered $&#123;baseCurrency&#125; is not covered
</li> </li>
<li i18n="@@rule.emergencyFundCoverage.false.unset">
No emergency fund amount has been set to determine the coverage
</li>
<li i18n="@@rule.emergencyFundCoverage.true"> <li i18n="@@rule.emergencyFundCoverage.true">
Your emergency fund of $&#123;emergencyFund&#125; Your emergency fund of $&#123;emergencyFund&#125;
$&#123;baseCurrency&#125; is covered $&#123;baseCurrency&#125; is covered

Loading…
Cancel
Save