Browse Source

Feature/add buying power to static portfolio analysis rules (#5365)

* Add buying power rule

* Update changelog
pull/5366/head
Thomas Kaul 5 days ago
committed by GitHub
parent
commit
52ea3c5e84
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 12
      apps/api/src/app/portfolio/portfolio.service.ts
  3. 7
      apps/api/src/app/user/user.service.ts
  4. 90
      apps/api/src/models/rules/liquidity/buying-power.ts
  5. 10
      apps/client/src/app/pages/i18n/i18n-page.html
  6. 24
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html
  7. 6
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
  8. 1
      libs/common/src/lib/interfaces/x-ray-rules-settings.interface.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added a new static portfolio analysis rule: _Liquidity_ (Buying Power)
- Added the interest and dividend values to the account detail dialog - Added the interest and dividend values to the account detail dialog
### Changed ### Changed

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

@ -15,6 +15,7 @@ import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/model
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 { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup'; import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup';
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment'; import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
import { BuyingPower } from '@ghostfolio/api/models/rules/liquidity/buying-power';
import { RegionalMarketClusterRiskAsiaPacific } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/asia-pacific'; import { RegionalMarketClusterRiskAsiaPacific } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/asia-pacific';
import { RegionalMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/emerging-markets'; import { RegionalMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/emerging-markets';
import { RegionalMarketClusterRiskEurope } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/europe'; import { RegionalMarketClusterRiskEurope } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/europe';
@ -1331,6 +1332,17 @@ export class PortfolioService {
], ],
userSettings userSettings
), ),
liquidity: await this.rulesService.evaluate(
[
new BuyingPower(
this.exchangeRateDataService,
this.i18nService,
summary.cash,
userSettings.language
)
],
userSettings
),
regionalMarketClusterRisk: regionalMarketClusterRisk:
summary.activityCount > 0 summary.activityCount > 0
? await this.rulesService.evaluate( ? await this.rulesService.evaluate(

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

@ -13,6 +13,7 @@ import { EconomicMarketClusterRiskDevelopedMarkets } from '@ghostfolio/api/model
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 { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup'; import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup';
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment'; import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
import { BuyingPower } from '@ghostfolio/api/models/rules/liquidity/buying-power';
import { RegionalMarketClusterRiskAsiaPacific } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/asia-pacific'; import { RegionalMarketClusterRiskAsiaPacific } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/asia-pacific';
import { RegionalMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/emerging-markets'; import { RegionalMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/emerging-markets';
import { RegionalMarketClusterRiskEurope } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/europe'; import { RegionalMarketClusterRiskEurope } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/europe';
@ -287,6 +288,12 @@ export class UserService {
undefined, undefined,
undefined undefined
).getSettings(user.settings.settings), ).getSettings(user.settings.settings),
BuyingPower: new BuyingPower(
undefined,
undefined,
undefined,
undefined
).getSettings(user.settings.settings),
CurrencyClusterRiskBaseCurrencyCurrentInvestment: CurrencyClusterRiskBaseCurrencyCurrentInvestment:
new CurrencyClusterRiskBaseCurrencyCurrentInvestment( new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
undefined, undefined,

90
apps/api/src/models/rules/liquidity/buying-power.ts

@ -0,0 +1,90 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
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 { UserSettings } from '@ghostfolio/common/interfaces';
export class BuyingPower extends Rule<Settings> {
private buyingPower: number;
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private i18nService: I18nService,
buyingPower: number,
languageCode: string
) {
super(exchangeRateDataService, {
languageCode,
key: BuyingPower.name
});
this.buyingPower = buyingPower;
}
public evaluate(ruleSettings: Settings) {
if (this.buyingPower < ruleSettings.thresholdMin) {
return {
evaluation: this.i18nService.getTranslation({
id: 'rule.liquidityBuyingPower.false',
languageCode: this.getLanguageCode(),
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
thresholdMin: ruleSettings.thresholdMin
}
}),
value: false
};
}
return {
evaluation: this.i18nService.getTranslation({
id: 'rule.liquidityBuyingPower.true',
languageCode: this.getLanguageCode(),
placeholders: {
baseCurrency: ruleSettings.baseCurrency,
thresholdMin: ruleSettings.thresholdMin
}
}),
value: true
};
}
public getCategoryName() {
return this.i18nService.getTranslation({
id: 'rule.liquidity.category',
languageCode: this.getLanguageCode()
});
}
public getConfiguration() {
return {
threshold: {
max: 200000,
min: 0,
step: 1000,
unit: ''
},
thresholdMin: true
};
}
public getName() {
return this.i18nService.getTranslation({
id: 'rule.liquidityBuyingPower',
languageCode: this.getLanguageCode()
});
}
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
return {
baseCurrency,
isActive: xRayRules?.[this.getKey()].isActive ?? true,
thresholdMin: xRayRules?.[this.getKey()]?.thresholdMin ?? 0
};
}
}
interface Settings extends RuleSettings {
baseCurrency: string;
thresholdMin: number;
}

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

@ -67,6 +67,16 @@
($&#123;fixedIncomeValueRatio&#125;%) is within the range of ($&#123;fixedIncomeValueRatio&#125;%) is within the range of
$&#123;thresholdMin&#125;% and $&#123;thresholdMax&#125;% $&#123;thresholdMin&#125;% and $&#123;thresholdMax&#125;%
</li> </li>
<li i18n="@@rule.liquidity.category">Liquidity</li>
<li i18n="@@rule.liquidityBuyingPower">Buying Power</li>
<li i18n="@@rule.liquidityBuyingPower.false">
Your buying power is below $&#123;thresholdMin&#125;
$&#123;baseCurrency&#125;
</li>
<li i18n="@@rule.liquidityBuyingPower.true">
Your buying power exceeds $&#123;thresholdMin&#125;
$&#123;baseCurrency&#125;
</li>
<li i18n="@@rule.currencyClusterRisk.category">Currency Cluster Risks</li> <li i18n="@@rule.currencyClusterRisk.category">Currency Cluster Risks</li>
<li i18n="@@rule.currencyClusterRiskBaseCurrencyCurrentInvestment"> <li i18n="@@rule.currencyClusterRiskBaseCurrencyCurrentInvestment">
Investment: Base Currency Investment: Base Currency

24
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.html

@ -59,6 +59,30 @@
</div> </div>
} }
</div> </div>
<div
class="mb-4"
[ngClass]="{
'd-none': liquidityRules?.length === 0
}"
>
<h4 class="align-items-center d-flex m-0">
<span i18n>Liquidity</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</h4>
<gf-rules
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId &&
hasPermissionToUpdateUserSettings &&
user?.settings?.isExperimentalFeatures
"
[isLoading]="isLoading"
[rules]="liquidityRules"
[settings]="user?.settings?.xRayRules"
(rulesUpdated)="onRulesUpdated($event)"
/>
</div>
<div <div
class="mb-4" class="mb-4"
[ngClass]="{ [ngClass]="{

6
apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts

@ -46,6 +46,7 @@ export class GfXRayPageComponent {
public hasPermissionToUpdateUserSettings: boolean; public hasPermissionToUpdateUserSettings: boolean;
public inactiveRules: PortfolioReportRule[]; public inactiveRules: PortfolioReportRule[];
public isLoading = false; public isLoading = false;
public liquidityRules: PortfolioReportRule[];
public regionalMarketClusterRiskRules: PortfolioReportRule[]; public regionalMarketClusterRiskRules: PortfolioReportRule[];
public statistics: PortfolioReportResponse['xRay']['statistics']; public statistics: PortfolioReportResponse['xRay']['statistics'];
public user: User; public user: User;
@ -149,6 +150,11 @@ export class GfXRayPageComponent {
return isActive; return isActive;
}) ?? null; }) ?? null;
this.liquidityRules =
rules['liquidity']?.filter(({ isActive }) => {
return isActive;
}) ?? null;
this.regionalMarketClusterRiskRules = this.regionalMarketClusterRiskRules =
rules['regionalMarketClusterRisk']?.filter(({ isActive }) => { rules['regionalMarketClusterRisk']?.filter(({ isActive }) => {
return isActive; return isActive;

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

@ -3,6 +3,7 @@ export interface XRayRulesSettings {
AccountClusterRiskSingleAccount?: RuleSettings; AccountClusterRiskSingleAccount?: RuleSettings;
AssetClassClusterRiskEquity?: RuleSettings; AssetClassClusterRiskEquity?: RuleSettings;
AssetClassClusterRiskFixedIncome?: RuleSettings; AssetClassClusterRiskFixedIncome?: RuleSettings;
BuyingPower?: RuleSettings;
CurrencyClusterRiskBaseCurrencyCurrentInvestment?: RuleSettings; CurrencyClusterRiskBaseCurrencyCurrentInvestment?: RuleSettings;
CurrencyClusterRiskCurrentInvestment?: RuleSettings; CurrencyClusterRiskCurrentInvestment?: RuleSettings;
EconomicMarketClusterRiskDevelopedMarkets?: RuleSettings; EconomicMarketClusterRiskDevelopedMarkets?: RuleSettings;

Loading…
Cancel
Save