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

72
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 { RuleSettings, UserSettings } from '@ghostfolio/common/interfaces';
export class EmergencyFundCoverage extends Rule<Settings> {
private cashBalanceInBaseCurrency: number;
private emergencyFundHoldingsValueInBaseCurrency: number;
private emergencyFundInBaseCurrency: number;
import { Big } from 'big.js';
export class EmergencyFundCoverage extends Rule<Settings> {
public constructor(
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
emergencyFundInBaseCurrency: number,
emergencyFundHoldingsValueInBaseCurrency: number,
cashBalanceInBaseCurrency: number
private emergencyFundInBaseCurrency: number,
private emergencyFundHoldingsValueInBaseCurrency: number,
private cashBalanceInBaseCurrency: number
) {
super(exchangeRateDataService, {
languageCode,
key: EmergencyFundCoverage.name
});
this.cashBalanceInBaseCurrency = cashBalanceInBaseCurrency;
this.emergencyFundHoldingsValueInBaseCurrency =
emergencyFundHoldingsValueInBaseCurrency;
this.emergencyFundInBaseCurrency = emergencyFundInBaseCurrency;
}
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,
// the cash balance covers the remainder
if (
this.emergencyFundHoldingsValueInBaseCurrency >
new Big(this.emergencyFundHoldingsValueInBaseCurrency).gt(
this.emergencyFundInBaseCurrency
)
) {
return {
evaluation: this.i18nService.getTranslation({
placeholders,
id: 'rule.emergencyFundCoverage.false.over',
languageCode: this.getLanguageCode(),
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString(
ruleSettings.locale
)
}
languageCode: this.getLanguageCode()
}),
value: false
};
}
const coverageInBaseCurrency =
this.emergencyFundHoldingsValueInBaseCurrency +
this.cashBalanceInBaseCurrency;
const coverageInBaseCurrency = new Big(
this.emergencyFundHoldingsValueInBaseCurrency
).plus(this.cashBalanceInBaseCurrency);
if (coverageInBaseCurrency < this.emergencyFundInBaseCurrency) {
if (coverageInBaseCurrency.lt(this.emergencyFundInBaseCurrency)) {
return {
evaluation: this.i18nService.getTranslation({
placeholders,
id: 'rule.emergencyFundCoverage.false.under',
languageCode: this.getLanguageCode(),
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString(
ruleSettings.locale
)
}
languageCode: this.getLanguageCode()
}),
value: false
};
@ -72,14 +73,9 @@ export class EmergencyFundCoverage extends Rule<Settings> {
return {
evaluation: this.i18nService.getTranslation({
placeholders,
id: 'rule.emergencyFundCoverage.true',
languageCode: this.getLanguageCode(),
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString(
ruleSettings.locale
)
}
languageCode: this.getLanguageCode()
}),
value: true
};

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

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

Loading…
Cancel
Save