refactor rule evaluation
This commit is contained in:
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]) => ({
|
||||
return Array.from(groupBy(attribute, positions).entries()).map(
|
||||
([attributeValue, objs]) => ({
|
||||
groupKey: attributeValue,
|
||||
investment: objs.reduce(
|
||||
(previousValue, currentValue) =>
|
||||
previousValue + currentValue.investment,
|
||||
previousValue + currentValue.investment.toNumber(),
|
||||
0
|
||||
),
|
||||
value: objs.reduce(
|
||||
(previousValue, currentValue) =>
|
||||
previousValue +
|
||||
this.exchangeRateDataService.toCurrency(
|
||||
currentValue.quantity * currentValue.marketPrice,
|
||||
currentValue.quantity.mul(currentValue.marketPrice).toNumber(),
|
||||
currentValue.currency,
|
||||
aBaseCurrency
|
||||
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 {
|
||||
for (const account of Object.keys(this.accounts)) {
|
||||
accounts[account] = {
|
||||
investment: current,
|
||||
name: 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 {
|
||||
for (const account of Object.keys(this.accounts)) {
|
||||
platforms[account] = {
|
||||
investment: original,
|
||||
name: 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…
x
Reference in New Issue
Block a user