Browse Source

Feature/modernize rules implementation (#3869)

* Modernize rules implementation

* Include markets in details
main
Thomas Kaul 14 hours ago
committed by GitHub
parent
commit
bcc8cb1e5b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      apps/api/src/app/portfolio/portfolio.controller.ts
  2. 172
      apps/api/src/app/portfolio/portfolio.service.ts
  3. 19
      apps/api/src/models/rule.ts
  4. 19
      apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts
  5. 17
      apps/api/src/models/rules/currency-cluster-risk/current-investment.ts
  6. 2
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts
  7. 7
      libs/common/src/lib/interfaces/portfolio-details.interface.ts

2
apps/api/src/app/portfolio/portfolio.controller.ts

@ -1,4 +1,3 @@
import { AccessService } from '@ghostfolio/api/app/access/access.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
@ -61,7 +60,6 @@ import { UpdateHoldingTagsDto } from './update-holding-tags.dto';
@Controller('portfolio')
export class PortfolioController {
public constructor(
private readonly accessService: AccessService,
private readonly apiService: ApiService,
private readonly configurationService: ConfigurationService,
private readonly impersonationService: ImpersonationService,

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

@ -45,6 +45,7 @@ import type {
AccountWithValue,
DateRange,
GroupBy,
Market,
RequestWithUser,
UserWithSettings
} from '@ghostfolio/common/types';
@ -581,6 +582,17 @@ export class PortfolioService {
};
}
let markets: {
[key in Market]: {
name: string;
value: number;
};
};
if (withMarkets) {
markets = this.getAggregatedMarkets(holdings);
}
let summary: PortfolioSummary;
if (withSummary) {
@ -602,6 +614,7 @@ export class PortfolioService {
accounts,
hasErrors,
holdings,
markets,
platforms,
summary
};
@ -1148,74 +1161,49 @@ export class PortfolioService {
public async getReport(impersonationId: string): Promise<PortfolioReport> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user);
const { activities } =
await this.orderService.getOrdersForPortfolioCalculator({
userCurrency,
userId
});
const userSettings = <UserSettings>this.request.user.Settings.settings;
const portfolioCalculator = this.calculatorFactory.createCalculator({
activities,
const { accounts, holdings, summary } = await this.getDetails({
impersonationId,
userId,
calculationType: PerformanceCalculationType.TWR,
currency: this.request.user.Settings.settings.baseCurrency
});
let { totalFeesWithCurrencyEffect, positions, totalInvestment } =
await portfolioCalculator.getSnapshot();
positions = positions.filter((item) => !item.quantity.eq(0));
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
for (const position of positions) {
portfolioItemsNow[position.symbol] = position;
}
const { accounts } = await this.getValueOfAccountsAndPlatforms({
activities,
portfolioItemsNow,
userCurrency,
userId
withMarkets: true,
withSummary: true
});
const userSettings = <UserSettings>this.request.user.Settings.settings;
return {
rules: {
accountClusterRisk: isEmpty(activities)
? undefined
: await this.rulesService.evaluate(
[
new AccountClusterRiskCurrentInvestment(
this.exchangeRateDataService,
accounts
),
new AccountClusterRiskSingleAccount(
this.exchangeRateDataService,
accounts
)
],
userSettings
),
currencyClusterRisk: isEmpty(activities)
? undefined
: await this.rulesService.evaluate(
[
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
this.exchangeRateDataService,
positions
),
new CurrencyClusterRiskCurrentInvestment(
this.exchangeRateDataService,
positions
)
],
userSettings
),
accountClusterRisk:
summary.ordersCount > 0
? await this.rulesService.evaluate(
[
new AccountClusterRiskCurrentInvestment(
this.exchangeRateDataService,
accounts
),
new AccountClusterRiskSingleAccount(
this.exchangeRateDataService,
accounts
)
],
userSettings
)
: undefined,
currencyClusterRisk:
summary.ordersCount > 0
? await this.rulesService.evaluate(
[
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
this.exchangeRateDataService,
Object.values(holdings)
),
new CurrencyClusterRiskCurrentInvestment(
this.exchangeRateDataService,
Object.values(holdings)
)
],
userSettings
)
: undefined,
emergencyFund: await this.rulesService.evaluate(
[
new EmergencyFundSetup(
@ -1229,8 +1217,8 @@ export class PortfolioService {
[
new FeeRatioInitialInvestment(
this.exchangeRateDataService,
totalInvestment.toNumber(),
totalFeesWithCurrencyEffect.toNumber()
summary.committedFunds,
summary.fees
)
],
userSettings
@ -1257,6 +1245,62 @@ export class PortfolioService {
await this.orderService.assignTags({ dataSource, symbol, tags, userId });
}
private getAggregatedMarkets(holdings: {
[symbol: string]: PortfolioPosition;
}): {
[key in Market]: { name: string; value: number };
} {
const markets = {
[UNKNOWN_KEY]: {
name: UNKNOWN_KEY,
value: 0
},
developedMarkets: {
name: 'developedMarkets',
value: 0
},
emergingMarkets: {
name: 'emergingMarkets',
value: 0
},
otherMarkets: {
name: 'otherMarkets',
value: 0
}
};
for (const [symbol, position] of Object.entries(holdings)) {
const value = position.valueInBaseCurrency;
if (position.assetClass !== AssetClass.LIQUIDITY) {
if (position.countries.length > 0) {
markets.developedMarkets.value +=
position.markets.developedMarkets * value;
markets.emergingMarkets.value +=
position.markets.emergingMarkets * value;
markets.otherMarkets.value += position.markets.otherMarkets * value;
} else {
markets[UNKNOWN_KEY].value += value;
}
}
}
const marketsTotal =
markets.developedMarkets.value +
markets.emergingMarkets.value +
markets.otherMarkets.value +
markets[UNKNOWN_KEY].value;
markets.developedMarkets.value =
markets.developedMarkets.value / marketsTotal;
markets.emergingMarkets.value =
markets.emergingMarkets.value / marketsTotal;
markets.otherMarkets.value = markets.otherMarkets.value / marketsTotal;
markets[UNKNOWN_KEY].value = markets[UNKNOWN_KEY].value / marketsTotal;
return markets;
}
private async getCashPositions({
cashDetails,
userCurrency,

19
apps/api/src/models/rule.ts

@ -1,8 +1,9 @@
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { groupBy } from '@ghostfolio/common/helper';
import { UserSettings } from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models';
import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces';
import { Big } from 'big.js';
import { EvaluationResult } from './interfaces/evaluation-result.interface';
import { RuleInterface } from './interfaces/rule.interface';
@ -33,24 +34,26 @@ export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
return this.name;
}
public groupCurrentPositionsByAttribute(
positions: TimelinePosition[],
attribute: keyof TimelinePosition,
public groupCurrentHoldingsByAttribute(
holdings: PortfolioPosition[],
attribute: keyof PortfolioPosition,
baseCurrency: string
) {
return Array.from(groupBy(attribute, positions).entries()).map(
return Array.from(groupBy(attribute, holdings).entries()).map(
([attributeValue, objs]) => ({
groupKey: attributeValue,
investment: objs.reduce(
(previousValue, currentValue) =>
previousValue + currentValue.investment.toNumber(),
previousValue + currentValue.investment,
0
),
value: objs.reduce(
(previousValue, currentValue) =>
previousValue +
this.exchangeRateDataService.toCurrency(
currentValue.quantity.mul(currentValue.marketPrice).toNumber(),
new Big(currentValue.quantity)
.mul(currentValue.marketPrice)
.toNumber(),
currentValue.currency,
baseCurrency
),

19
apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts

@ -1,35 +1,34 @@
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 { UserSettings } from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models';
import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces';
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> {
private positions: TimelinePosition[];
private holdings: PortfolioPosition[];
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
positions: TimelinePosition[]
holdings: PortfolioPosition[]
) {
super(exchangeRateDataService, {
key: CurrencyClusterRiskBaseCurrencyCurrentInvestment.name,
name: 'Investment: Base Currency'
});
this.positions = positions;
this.holdings = holdings;
}
public evaluate(ruleSettings: Settings) {
const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute(
this.positions,
const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute(
this.holdings,
'currency',
ruleSettings.baseCurrency
);
let maxItem = positionsGroupedByCurrency[0];
let maxItem = holdingsGroupedByCurrency[0];
let totalValue = 0;
positionsGroupedByCurrency.forEach((groupItem) => {
holdingsGroupedByCurrency.forEach((groupItem) => {
// Calculate total value
totalValue += groupItem.value;
@ -39,7 +38,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
}
});
const baseCurrencyItem = positionsGroupedByCurrency.find((item) => {
const baseCurrencyItem = holdingsGroupedByCurrency.find((item) => {
return item.groupKey === ruleSettings.baseCurrency;
});

17
apps/api/src/models/rules/currency-cluster-risk/current-investment.ts

@ -1,35 +1,34 @@
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 { UserSettings } from '@ghostfolio/common/interfaces';
import { TimelinePosition } from '@ghostfolio/common/models';
import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces';
export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
private positions: TimelinePosition[];
private holdings: PortfolioPosition[];
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
positions: TimelinePosition[]
holdings: PortfolioPosition[]
) {
super(exchangeRateDataService, {
key: CurrencyClusterRiskCurrentInvestment.name,
name: 'Investment'
});
this.positions = positions;
this.holdings = holdings;
}
public evaluate(ruleSettings: Settings) {
const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute(
this.positions,
const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute(
this.holdings,
'currency',
ruleSettings.baseCurrency
);
let maxItem = positionsGroupedByCurrency[0];
let maxItem = holdingsGroupedByCurrency[0];
let totalValue = 0;
positionsGroupedByCurrency.forEach((groupItem) => {
holdingsGroupedByCurrency.forEach((groupItem) => {
// Calculate total value
totalValue += groupItem.value;

2
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts

@ -35,8 +35,6 @@ export class GfRuleSettingsDialogComponent {
@Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams,
public dialogRef: MatDialogRef<GfRuleSettingsDialogComponent>
) {
console.log(this.data.rule);
this.settings = this.data.rule.settings;
}
}

7
libs/common/src/lib/interfaces/portfolio-details.interface.ts

@ -2,6 +2,7 @@ import {
PortfolioPosition,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
import { Market } from '@ghostfolio/common/types';
export interface PortfolioDetails {
accounts: {
@ -14,6 +15,12 @@ export interface PortfolioDetails {
};
};
holdings: { [symbol: string]: PortfolioPosition };
markets?: {
[key in Market]: {
name: string;
value: number;
};
};
platforms: {
[id: string]: {
balance: number;

Loading…
Cancel
Save