refactor rule evaluation

pull/239/head
Valentin Zickner 3 years ago committed by Thomas
parent 04e6518226
commit 23b2e03923

@ -0,0 +1,11 @@
import { TimelinePosition } from '@ghostfolio/common/interfaces';
import Big from 'big.js';
export interface CurrentPositions {
hasErrors: boolean;
positions: TimelinePosition[];
grossPerformance: Big;
grossPerformancePercentage: Big;
currentValue: Big;
totalInvestment: Big;
}

@ -26,6 +26,7 @@ import {
subDays
} from 'date-fns';
import { flatten } from 'lodash';
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface';
export class PortfolioCalculator {
private transactionPoints: TransactionPoint[];
@ -111,14 +112,7 @@ export class PortfolioCalculator {
this.transactionPoints = transactionPoints;
}
public async getCurrentPositions(start: Date): Promise<{
hasErrors: boolean;
positions: TimelinePosition[];
grossPerformance: Big;
grossPerformancePercentage: Big;
currentValue: Big;
totalInvestment: Big;
}> {
public async getCurrentPositions(start: Date): Promise<CurrentPositions> {
if (!this.transactionPoints?.length) {
return {
hasErrors: false,

@ -4,18 +4,14 @@ import { PortfolioOrder } from '@ghostfolio/api/app/core/interfaces/portfolio-or
import { TimelineSpecification } from '@ghostfolio/api/app/core/interfaces/timeline-specification.interface';
import { PortfolioCalculator } from '@ghostfolio/api/app/core/portfolio-calculator';
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { OrderType } from '@ghostfolio/api/models/order-type';
import { Portfolio } from '@ghostfolio/api/models/portfolio';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { IOrder, Type } from '@ghostfolio/api/services/interfaces/interfaces';
import { Type } from '@ghostfolio/api/services/interfaces/interfaces';
import { RulesService } from '@ghostfolio/api/services/rules.service';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import {
PortfolioItem,
PortfolioOverview,
PortfolioPerformance,
PortfolioPosition,
@ -30,11 +26,10 @@ import {
} from '@ghostfolio/common/types';
import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { DataSource, Currency, Type as TypeOfOrder } from '@prisma/client';
import { Currency, DataSource, Type as TypeOfOrder } from '@prisma/client';
import Big from 'big.js';
import {
add,
addMonths,
endOfToday,
format,
getDate,
@ -42,7 +37,6 @@ import {
getYear,
isAfter,
isBefore,
isSameDay,
max,
parse,
parseISO,
@ -54,7 +48,6 @@ import {
subYears
} from 'date-fns';
import { isEmpty } from 'lodash';
import * as roundTo from 'round-to';
import {
HistoricalDataItem,
@ -83,69 +76,11 @@ export class PortfolioService {
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly impersonationService: ImpersonationService,
private readonly orderService: OrderService,
private readonly redisCacheService: RedisCacheService,
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly rulesService: RulesService,
private readonly userService: UserService,
private readonly symbolProfileService: SymbolProfileService
) {}
public async createPortfolio(aUserId: string): Promise<Portfolio> {
let portfolio: Portfolio;
const stringifiedPortfolio = await this.redisCacheService.get(
`${aUserId}.portfolio`
);
const user = await this.userService.user({ id: aUserId });
if (stringifiedPortfolio) {
// Get portfolio from redis
const {
orders,
portfolioItems
}: { orders: IOrder[]; portfolioItems: PortfolioItem[] } =
JSON.parse(stringifiedPortfolio);
portfolio = new Portfolio(
this.accountService,
this.dataProviderService,
this.exchangeRateDataService,
this.rulesService
).createFromData({ orders, portfolioItems, user });
} else {
// Get portfolio from database
const orders = await this.getOrders(aUserId);
portfolio = new Portfolio(
this.accountService,
this.dataProviderService,
this.exchangeRateDataService,
this.rulesService
);
portfolio.setUser(user);
await portfolio.setOrders(orders);
// Cache data for the next time...
const portfolioData = {
orders: portfolio.getOrders(),
portfolioItems: portfolio.getPortfolioItems()
};
await this.redisCacheService.set(
`${aUserId}.portfolio`,
JSON.stringify(portfolioData)
);
}
// Enrich portfolio with current data
await portfolio.addCurrentPortfolioItems();
// Enrich portfolio with future data
await portfolio.addFuturePortfolioItems();
return portfolio;
}
public async getInvestments(
aImpersonationId: string
): Promise<InvestmentItem[]> {
@ -603,35 +538,49 @@ export class PortfolioService {
public async getReport(impersonationId: string): Promise<PortfolioReport> {
const userId = await this.getUserId(impersonationId);
const portfolio = await this.createPortfolio(userId);
const baseCurrency = this.request.user.Settings.currency;
const details = await portfolio.getDetails();
const { orders } = await this.getTransactionPoints(userId);
const { transactionPoints, orders } = await this.getTransactionPoints(
userId
);
if (isEmpty(details)) {
if (isEmpty(orders)) {
return {
rules: {}
};
}
const fees = this.getFees(orders);
const portfolioCalculator = new PortfolioCalculator(
this.currentRateService,
this.request.user.Settings.currency
);
portfolioCalculator.setTransactionPoints(transactionPoints);
const baseCurrency = this.request.user.Settings.currency;
const portfolioStart = parseDate(transactionPoints[0].date);
const currentPositions = await portfolioCalculator.getCurrentPositions(
portfolioStart
);
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
for (const position of currentPositions.positions) {
portfolioItemsNow[position.symbol] = position;
}
const accounts = this.getAccounts(orders, portfolioItemsNow, baseCurrency);
return {
rules: {
accountClusterRisk: await this.rulesService.evaluate(
[
new AccountClusterRiskInitialInvestment(
this.exchangeRateDataService,
details
accounts
),
new AccountClusterRiskCurrentInvestment(
this.exchangeRateDataService,
details
accounts
),
new AccountClusterRiskSingleAccount(
this.exchangeRateDataService,
details
accounts
)
],
{ baseCurrency }
@ -640,19 +589,19 @@ export class PortfolioService {
[
new CurrencyClusterRiskBaseCurrencyInitialInvestment(
this.exchangeRateDataService,
details
currentPositions
),
new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
this.exchangeRateDataService,
details
currentPositions
),
new CurrencyClusterRiskInitialInvestment(
this.exchangeRateDataService,
details
currentPositions
),
new CurrencyClusterRiskCurrentInvestment(
this.exchangeRateDataService,
details
currentPositions
)
],
{ baseCurrency }
@ -661,8 +610,8 @@ export class PortfolioService {
[
new FeeRatioInitialInvestment(
this.exchangeRateDataService,
details,
fees
currentPositions.totalInvestment.toNumber(),
this.getFees(orders)
)
],
{ baseCurrency }

@ -1,5 +1,8 @@
import { groupBy } from '@ghostfolio/common/helper';
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import {
PortfolioPosition,
TimelinePosition
} from '@ghostfolio/common/interfaces';
import { Currency } from '@prisma/client';
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
@ -30,30 +33,30 @@ export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
return this.name;
}
public groupPositionsByAttribute(
aPositions: { [symbol: string]: PortfolioPosition },
aAttribute: keyof PortfolioPosition,
aBaseCurrency: Currency
public groupCurrentPositionsByAttribute(
positions: TimelinePosition[],
attribute: keyof TimelinePosition,
baseCurrency: Currency
) {
return Array.from(
groupBy(aAttribute, Object.values(aPositions)).entries()
).map(([attributeValue, objs]) => ({
groupKey: attributeValue,
investment: objs.reduce(
(previousValue, currentValue) =>
previousValue + currentValue.investment,
0
),
value: objs.reduce(
(previousValue, currentValue) =>
previousValue +
this.exchangeRateDataService.toCurrency(
currentValue.quantity * currentValue.marketPrice,
currentValue.currency,
aBaseCurrency
),
0
)
}));
return Array.from(groupBy(attribute, positions).entries()).map(
([attributeValue, objs]) => ({
groupKey: attributeValue,
investment: objs.reduce(
(previousValue, currentValue) =>
previousValue + currentValue.investment.toNumber(),
0
),
value: objs.reduce(
(previousValue, currentValue) =>
previousValue +
this.exchangeRateDataService.toCurrency(
currentValue.quantity.mul(currentValue.marketPrice).toNumber(),
currentValue.currency,
baseCurrency
),
0
)
})
);
}
}

@ -8,7 +8,9 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.in
export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private positions: { [symbol: string]: PortfolioPosition }
private accounts: {
[account: string]: { current: number; original: number };
}
) {
super(exchangeRateDataService, {
name: 'Current Investment'
@ -22,18 +24,12 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
};
} = {};
Object.values(this.positions).forEach((position) => {
for (const [account, { current }] of Object.entries(position.accounts)) {
if (accounts[account]?.investment) {
accounts[account].investment += current;
} else {
accounts[account] = {
investment: current,
name: account
};
}
}
});
for (const account of Object.keys(this.accounts)) {
accounts[account] = {
name: account,
investment: this.accounts[account].current
};
}
let maxItem;
let totalInvestment = 0;

@ -8,7 +8,9 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.in
export class AccountClusterRiskInitialInvestment extends Rule<Settings> {
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private positions: { [symbol: string]: PortfolioPosition }
private accounts: {
[account: string]: { current: number; original: number };
}
) {
super(exchangeRateDataService, {
name: 'Initial Investment'
@ -22,18 +24,12 @@ export class AccountClusterRiskInitialInvestment extends Rule<Settings> {
};
} = {};
Object.values(this.positions).forEach((position) => {
for (const [account, { original }] of Object.entries(position.accounts)) {
if (platforms[account]?.investment) {
platforms[account].investment += original;
} else {
platforms[account] = {
investment: original,
name: account
};
}
}
});
for (const account of Object.keys(this.accounts)) {
platforms[account] = {
name: account,
investment: this.accounts[account].original
};
}
let maxItem;
let totalInvestment = 0;

@ -1,4 +1,3 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule';
@ -8,7 +7,9 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.in
export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private positions: { [symbol: string]: PortfolioPosition }
private accounts: {
[account: string]: { current: number; original: number };
}
) {
super(exchangeRateDataService, {
name: 'Single Account'
@ -16,15 +17,7 @@ export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
}
public evaluate() {
const accounts: string[] = [];
Object.values(this.positions).forEach((position) => {
for (const [account] of Object.entries(position.accounts)) {
if (!accounts.includes(account)) {
accounts.push(account);
}
}
});
const accounts: string[] = Object.keys(this.accounts);
if (accounts.length === 1) {
return {

@ -5,11 +5,12 @@ import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { Rule } from '../../rule';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface';
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> {
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private positions: { [symbol: string]: PortfolioPosition }
private currentPositions: CurrentPositions
) {
super(exchangeRateDataService, {
name: 'Current Investment: Base Currency'
@ -17,8 +18,8 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
}
public evaluate(ruleSettings: Settings) {
const positionsGroupedByCurrency = this.groupPositionsByAttribute(
this.positions,
const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute(
this.currentPositions.positions,
'currency',
ruleSettings.baseCurrency
);

@ -1,15 +1,15 @@
import { Currency } from '@prisma/client';
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface';
export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule<Settings> {
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private positions: { [symbol: string]: PortfolioPosition }
private currentPositions: CurrentPositions
) {
super(exchangeRateDataService, {
name: 'Initial Investment: Base Currency'
@ -17,8 +17,8 @@ export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule<Setti
}
public evaluate(ruleSettings: Settings) {
const positionsGroupedByCurrency = this.groupPositionsByAttribute(
this.positions,
const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute(
this.currentPositions.positions,
'currency',
ruleSettings.baseCurrency
);

@ -1,15 +1,15 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { Currency } from '@prisma/client';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface';
export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
public constructor(
public exchangeRateDataService: ExchangeRateDataService,
private positions: { [symbol: string]: PortfolioPosition }
private currentPositions: CurrentPositions
) {
super(exchangeRateDataService, {
name: 'Current Investment'
@ -17,8 +17,8 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
}
public evaluate(ruleSettings: Settings) {
const positionsGroupedByCurrency = this.groupPositionsByAttribute(
this.positions,
const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute(
this.currentPositions.positions,
'currency',
ruleSettings.baseCurrency
);

@ -1,15 +1,15 @@
import { Currency } from '@prisma/client';
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule';
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
import { CurrentPositions } from '@ghostfolio/api/app/core/interfaces/current-positions.interface';
export class CurrencyClusterRiskInitialInvestment extends Rule<Settings> {
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private positions: { [symbol: string]: PortfolioPosition }
private currentPositions: CurrentPositions
) {
super(exchangeRateDataService, {
name: 'Initial Investment'
@ -17,8 +17,8 @@ export class CurrencyClusterRiskInitialInvestment extends Rule<Settings> {
}
public evaluate(ruleSettings: Settings) {
const positionsGroupedByCurrency = this.groupPositionsByAttribute(
this.positions,
const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute(
this.currentPositions.positions,
'currency',
ruleSettings.baseCurrency
);

@ -1,5 +1,4 @@
import { Currency } from '@prisma/client';
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
import { Rule } from '../../rule';
@ -9,7 +8,7 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.in
export class FeeRatioInitialInvestment extends Rule<Settings> {
public constructor(
protected exchangeRateDataService: ExchangeRateDataService,
private positions: { [symbol: string]: PortfolioPosition },
private totalInvestment: number,
private fees: number
) {
super(exchangeRateDataService, {
@ -18,20 +17,7 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
}
public evaluate(ruleSettings: Settings) {
const positionsGroupedByCurrency = this.groupPositionsByAttribute(
this.positions,
'currency',
ruleSettings.baseCurrency
);
let totalInvestment = 0;
positionsGroupedByCurrency.forEach((groupItem) => {
// Calculate total investment
totalInvestment += groupItem.investment;
});
const feeRatio = this.fees / totalInvestment;
const feeRatio = this.fees / this.totalInvestment;
if (feeRatio > ruleSettings.threshold) {
return {

Loading…
Cancel
Save