Browse Source

Add emergency fund coverage rule

pull/7538/head
Thomas Kaul 4 weeks ago
parent
commit
f426d0a186
  1. 33
      apps/api/src/app/portfolio/portfolio.service.ts
  2. 9
      apps/api/src/app/user/user.service.ts
  3. 121
      apps/api/src/models/rules/emergency-fund/emergency-fund-coverage.ts
  4. 16
      apps/client/src/app/pages/i18n/i18n-page.html
  5. 1
      libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts

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

@ -12,6 +12,7 @@ import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/ap
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment'; import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/developed-markets'; import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/developed-markets';
import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets'; import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets';
import { EmergencyFundCoverage } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-coverage';
import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup'; import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup';
import { FeeRatioTotalInvestmentVolume } from '@ghostfolio/api/models/rules/fees/fee-ratio-total-investment-volume'; import { FeeRatioTotalInvestmentVolume } from '@ghostfolio/api/models/rules/fees/fee-ratio-total-investment-volume';
import { BuyingPower } from '@ghostfolio/api/models/rules/liquidity/buying-power'; import { BuyingPower } from '@ghostfolio/api/models/rules/liquidity/buying-power';
@ -1137,6 +1138,17 @@ export class PortfolioService {
const hasOpenHoldings = Object.keys(holdings).length > 0; const hasOpenHoldings = Object.keys(holdings).length > 0;
const emergencyFundHoldingsValueInBaseCurrency =
this.getEmergencyFundHoldingsValueInBaseCurrency({ holdings });
const emergencyFundInBaseCurrency = userSettings.emergencyFund ?? 0;
const { balanceInBaseCurrency: cashBalanceInBaseCurrency } =
await this.accountService.getCashDetails({
userId,
currency: this.getUserCurrency()
});
const marketsAdvancedTotalInBaseCurrency = getSum( const marketsAdvancedTotalInBaseCurrency = getSum(
Object.values(marketsAdvanced).map(({ valueInBaseCurrency }) => { Object.values(marketsAdvanced).map(({ valueInBaseCurrency }) => {
return new Big(valueInBaseCurrency); return new Big(valueInBaseCurrency);
@ -1181,11 +1193,24 @@ export class PortfolioService {
this.i18nService, this.i18nService,
userSettings.language, userSettings.language,
this.getTotalEmergencyFund({ this.getTotalEmergencyFund({
userSettings, emergencyFundHoldingsValueInBaseCurrency,
emergencyFundHoldingsValueInBaseCurrency: userSettings
this.getEmergencyFundHoldingsValueInBaseCurrency({ holdings })
}).toNumber() }).toNumber()
) ),
// The coverage is only meaningful once a target has been set,
// otherwise the set up rule already reports the missing setup
...(emergencyFundInBaseCurrency > 0
? [
new EmergencyFundCoverage(
this.exchangeRateDataService,
this.i18nService,
userSettings.language,
emergencyFundInBaseCurrency,
emergencyFundHoldingsValueInBaseCurrency,
cashBalanceInBaseCurrency
)
]
: [])
], ],
userSettings userSettings
) )

9
apps/api/src/app/user/user.service.ts

@ -11,6 +11,7 @@ import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/ap
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment'; import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/developed-markets'; import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/developed-markets';
import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets'; import { EconomicMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/economic-market-cluster-risk/emerging-markets';
import { EmergencyFundCoverage } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-coverage';
import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup'; import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup';
import { FeeRatioTotalInvestmentVolume } from '@ghostfolio/api/models/rules/fees/fee-ratio-total-investment-volume'; import { FeeRatioTotalInvestmentVolume } from '@ghostfolio/api/models/rules/fees/fee-ratio-total-investment-volume';
import { BuyingPower } from '@ghostfolio/api/models/rules/liquidity/buying-power'; import { BuyingPower } from '@ghostfolio/api/models/rules/liquidity/buying-power';
@ -448,6 +449,14 @@ export class UserService {
undefined, undefined,
undefined undefined
).getSettings(user.settings.settings), ).getSettings(user.settings.settings),
EmergencyFundCoverage: new EmergencyFundCoverage(
undefined,
undefined,
undefined,
undefined,
undefined,
undefined
).getSettings(user.settings.settings),
EmergencyFundSetup: new EmergencyFundSetup( EmergencyFundSetup: new EmergencyFundSetup(
undefined, undefined,
undefined, undefined,

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

@ -0,0 +1,121 @@
import { Rule } from '@ghostfolio/api/models/rule';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
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;
public constructor(
exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
languageCode: string,
emergencyFundInBaseCurrency: number,
emergencyFundHoldingsValueInBaseCurrency: number,
cashBalanceInBaseCurrency: number
) {
super(exchangeRateDataService, {
languageCode,
key: EmergencyFundCoverage.name
});
this.cashBalanceInBaseCurrency = cashBalanceInBaseCurrency;
this.emergencyFundHoldingsValueInBaseCurrency =
emergencyFundHoldingsValueInBaseCurrency;
this.emergencyFundInBaseCurrency = emergencyFundInBaseCurrency;
}
public evaluate(ruleSettings: Settings) {
// Only the holdings tagged as emergency fund are an explicit commitment,
// the cash balance covers the remainder
if (
this.emergencyFundHoldingsValueInBaseCurrency >
this.emergencyFundInBaseCurrency
) {
return {
evaluation: this.i18nService.getTranslation({
id: 'rule.emergencyFundCoverage.false.over',
languageCode: this.getLanguageCode(),
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString(
ruleSettings.locale
),
emergencyFundHoldingsValue:
this.emergencyFundHoldingsValueInBaseCurrency.toLocaleString(
ruleSettings.locale
)
}
}),
value: false
};
}
const coverageInBaseCurrency =
this.emergencyFundHoldingsValueInBaseCurrency +
this.cashBalanceInBaseCurrency;
if (coverageInBaseCurrency < this.emergencyFundInBaseCurrency) {
return {
evaluation: this.i18nService.getTranslation({
id: 'rule.emergencyFundCoverage.false.under',
languageCode: this.getLanguageCode(),
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
coverage: coverageInBaseCurrency.toLocaleString(
ruleSettings.locale
),
emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString(
ruleSettings.locale
)
}
}),
value: false
};
}
return {
evaluation: this.i18nService.getTranslation({
id: 'rule.emergencyFundCoverage.true',
languageCode: this.getLanguageCode(),
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
emergencyFund: this.emergencyFundInBaseCurrency.toLocaleString(
ruleSettings.locale
)
}
}),
value: true
};
}
public getConfiguration() {
return undefined;
}
public getName() {
return this.i18nService.getTranslation({
id: 'rule.emergencyFundCoverage',
languageCode: this.getLanguageCode()
});
}
public getSettings({
baseCurrency = DEFAULT_CURRENCY,
locale = DEFAULT_LOCALE,
xRayRules
}: UserSettings): Settings {
return {
baseCurrency,
locale,
isActive: xRayRules?.[this.getKey()]?.isActive ?? true
};
}
}
interface Settings extends RuleSettings {
baseCurrency: string;
}

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

@ -142,6 +142,22 @@
$&#123;thresholdMin&#125;% and $&#123;thresholdMax&#125;% $&#123;thresholdMin&#125;% and $&#123;thresholdMax&#125;%
</li> </li>
<li i18n="@@rule.emergencyFund.category">Emergency Fund</li> <li i18n="@@rule.emergencyFund.category">Emergency Fund</li>
<li i18n="@@rule.emergencyFundCoverage">Coverage</li>
<li i18n="@@rule.emergencyFundCoverage.false.over">
The assets tagged as emergency fund
($&#123;emergencyFundHoldingsValue&#125; $&#123;baseCurrency&#125;)
exceed your emergency fund of $&#123;emergencyFund&#125;
$&#123;baseCurrency&#125;
</li>
<li i18n="@@rule.emergencyFundCoverage.false.under">
Your emergency fund of $&#123;emergencyFund&#125;
$&#123;baseCurrency&#125; is not covered ($&#123;coverage&#125;
$&#123;baseCurrency&#125; available)
</li>
<li i18n="@@rule.emergencyFundCoverage.true">
Your emergency fund of $&#123;emergencyFund&#125;
$&#123;baseCurrency&#125; is covered
</li>
<li i18n="@@rule.emergencyFundSetup">Set up</li> <li i18n="@@rule.emergencyFundSetup">Set up</li>
<li i18n="@@rule.emergencyFundSetup.false"> <li i18n="@@rule.emergencyFundSetup.false">
No emergency fund has been set up No emergency fund has been set up

1
libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts

@ -8,6 +8,7 @@ export interface XRayRulesSettings {
CurrencyClusterRiskCurrentInvestment?: RuleSettings; CurrencyClusterRiskCurrentInvestment?: RuleSettings;
EconomicMarketClusterRiskDevelopedMarkets?: RuleSettings; EconomicMarketClusterRiskDevelopedMarkets?: RuleSettings;
EconomicMarketClusterRiskEmergingMarkets?: RuleSettings; EconomicMarketClusterRiskEmergingMarkets?: RuleSettings;
EmergencyFundCoverage?: RuleSettings;
EmergencyFundSetup?: RuleSettings; EmergencyFundSetup?: RuleSettings;
FeeRatioTotalInvestmentVolume?: RuleSettings; FeeRatioTotalInvestmentVolume?: RuleSettings;
RegionalMarketClusterRiskAsiaPacific?: RuleSettings; RegionalMarketClusterRiskAsiaPacific?: RuleSettings;

Loading…
Cancel
Save