diff --git a/apps/api/src/models/interfaces/rule-settings.interface.ts b/apps/api/src/models/interfaces/rule-settings.interface.ts new file mode 100644 index 000000000..377bab52b --- /dev/null +++ b/apps/api/src/models/interfaces/rule-settings.interface.ts @@ -0,0 +1,3 @@ +export interface RuleSettings { + isActive: boolean; +} diff --git a/apps/api/src/models/interfaces/rule.interface.ts b/apps/api/src/models/interfaces/rule.interface.ts index 515730694..1de2e66a6 100644 --- a/apps/api/src/models/interfaces/rule.interface.ts +++ b/apps/api/src/models/interfaces/rule.interface.ts @@ -1,15 +1,17 @@ import { PortfolioPosition } from '@ghostfolio/common/interfaces'; +import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; import { EvaluationResult } from './evaluation-result.interface'; +import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; -export interface RuleInterface { +export interface RuleInterface { evaluate( aPortfolioPositionMap: { [symbol: string]: PortfolioPosition; }, aFees: number, - aRuleSettingsMap: { - [key: string]: any; - } + aRuleSettings: T ): EvaluationResult; + + getSettings(aUserSettings: UserSettings): T; } diff --git a/apps/api/src/models/interfaces/user-settings.interface.ts b/apps/api/src/models/interfaces/user-settings.interface.ts new file mode 100644 index 000000000..6cfffedca --- /dev/null +++ b/apps/api/src/models/interfaces/user-settings.interface.ts @@ -0,0 +1,5 @@ +import { Currency } from '@prisma/client'; + +export interface UserSettings { + baseCurrency: Currency; +} diff --git a/apps/api/src/models/portfolio.ts b/apps/api/src/models/portfolio.ts index 7b75622bc..9e58b3abf 100644 --- a/apps/api/src/models/portfolio.ts +++ b/apps/api/src/models/portfolio.ts @@ -445,10 +445,13 @@ export class Portfolio implements PortfolioInterface { }; } + const fees = this.getFees(); + return { rules: { accountClusterRisk: await this.rulesService.evaluate( - this, + details, + fees, [ new AccountClusterRiskInitialInvestment( this.exchangeRateDataService @@ -461,7 +464,8 @@ export class Portfolio implements PortfolioInterface { { baseCurrency: this.user.Settings.currency } ), currencyClusterRisk: await this.rulesService.evaluate( - this, + details, + fees, [ new CurrencyClusterRiskBaseCurrencyInitialInvestment( this.exchangeRateDataService @@ -479,7 +483,8 @@ export class Portfolio implements PortfolioInterface { { baseCurrency: this.user.Settings.currency } ), fees: await this.rulesService.evaluate( - this, + details, + fees, [new FeeRatioInitialInvestment(this.exchangeRateDataService)], { baseCurrency: this.user.Settings.currency } ) diff --git a/apps/api/src/models/rule.ts b/apps/api/src/models/rule.ts index 5e9194a1a..d5a4d3621 100644 --- a/apps/api/src/models/rule.ts +++ b/apps/api/src/models/rule.ts @@ -5,8 +5,10 @@ import { Currency } from '@prisma/client'; import { ExchangeRateDataService } from '../services/exchange-rate-data.service'; import { EvaluationResult } from './interfaces/evaluation-result.interface'; import { RuleInterface } from './interfaces/rule.interface'; +import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface'; +import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; -export abstract class Rule implements RuleInterface { +export abstract class Rule implements RuleInterface { private name: string; public constructor( @@ -25,11 +27,11 @@ export abstract class Rule implements RuleInterface { [symbol: string]: PortfolioPosition; }, aFees: number, - aRuleSettingsMap?: { - [key: string]: any; - } + aRuleSettings: T ): EvaluationResult; + public abstract getSettings(aUserSettings: UserSettings): T; + public getName() { return this.name; } diff --git a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts index d30957315..b6dbe86e0 100644 --- a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts +++ b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts @@ -2,8 +2,10 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- 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'; -export class AccountClusterRiskCurrentInvestment extends Rule { +export class AccountClusterRiskCurrentInvestment extends Rule { public constructor(public exchangeRateDataService: ExchangeRateDataService) { super(exchangeRateDataService, { name: 'Current Investment' @@ -13,13 +15,8 @@ export class AccountClusterRiskCurrentInvestment extends Rule { public evaluate( aPositions: { [symbol: string]: PortfolioPosition }, aFees: number, - aRuleSettingsMap?: { - [key: string]: any; - } + ruleSettings?: Settings ) { - const ruleSettings = - aRuleSettingsMap[AccountClusterRiskCurrentInvestment.name]; - const accounts: { [symbol: string]: Pick & { investment: number; @@ -78,4 +75,17 @@ export class AccountClusterRiskCurrentInvestment extends Rule { value: true }; } + + public getSettings(aUserSettings: UserSettings): Settings { + return { + baseCurrency: aUserSettings.baseCurrency, + isActive: true, + threshold: 0.5 + }; + } +} + +interface Settings extends RuleSettings { + baseCurrency: string; + threshold: number; } diff --git a/apps/api/src/models/rules/account-cluster-risk/initial-investment.ts b/apps/api/src/models/rules/account-cluster-risk/initial-investment.ts index 50ab10bec..2bf8da1f8 100644 --- a/apps/api/src/models/rules/account-cluster-risk/initial-investment.ts +++ b/apps/api/src/models/rules/account-cluster-risk/initial-investment.ts @@ -2,8 +2,10 @@ 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'; -export class AccountClusterRiskInitialInvestment extends Rule { +export class AccountClusterRiskInitialInvestment extends Rule { public constructor(public exchangeRateDataService: ExchangeRateDataService) { super(exchangeRateDataService, { name: 'Initial Investment' @@ -13,13 +15,8 @@ export class AccountClusterRiskInitialInvestment extends Rule { public evaluate( aPositions: { [symbol: string]: PortfolioPosition }, aFees: number, - aRuleSettingsMap?: { - [key: string]: any; - } + ruleSettings?: Settings ) { - const ruleSettings = - aRuleSettingsMap[AccountClusterRiskInitialInvestment.name]; - const platforms: { [symbol: string]: Pick & { investment: number; @@ -78,4 +75,18 @@ export class AccountClusterRiskInitialInvestment extends Rule { value: true }; } + + public getSettings(aUserSettings: UserSettings): Settings { + return { + baseCurrency: aUserSettings.baseCurrency, + isActive: true, + threshold: 0.5 + }; + } +} + +interface Settings extends RuleSettings { + baseCurrency: string; + isActive: boolean; + threshold: number; } diff --git a/apps/api/src/models/rules/account-cluster-risk/single-account.ts b/apps/api/src/models/rules/account-cluster-risk/single-account.ts index 011906270..86093de2e 100644 --- a/apps/api/src/models/rules/account-cluster-risk/single-account.ts +++ b/apps/api/src/models/rules/account-cluster-risk/single-account.ts @@ -2,8 +2,10 @@ 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'; -export class AccountClusterRiskSingleAccount extends Rule { +export class AccountClusterRiskSingleAccount extends Rule { public constructor(public exchangeRateDataService: ExchangeRateDataService) { super(exchangeRateDataService, { name: 'Single Account' @@ -33,4 +35,10 @@ export class AccountClusterRiskSingleAccount extends Rule { value: true }; } + + public getSettings(aUserSettings: UserSettings): RuleSettings { + return { + isActive: true + }; + } } diff --git a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts index a09e57c95..826bb1b55 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts @@ -1,9 +1,12 @@ +import { Currency } from '@prisma/client'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; 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'; -export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { +export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { public constructor(public exchangeRateDataService: ExchangeRateDataService) { super(exchangeRateDataService, { name: 'Current Investment: Base Currency' @@ -13,13 +16,8 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { public evaluate( aPositions: { [symbol: string]: PortfolioPosition }, aFees: number, - aRuleSettingsMap?: { - [key: string]: any; - } + ruleSettings: Settings ) { - const ruleSettings = - aRuleSettingsMap[CurrencyClusterRiskBaseCurrencyCurrentInvestment.name]; - const positionsGroupedByCurrency = this.groupPositionsByAttribute( aPositions, 'currency', @@ -61,4 +59,15 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { value: true }; } + + public getSettings(aUserSettings: UserSettings): Settings { + return { + baseCurrency: aUserSettings.baseCurrency, + isActive: true + }; + } +} + +interface Settings extends RuleSettings { + baseCurrency: Currency; } diff --git a/apps/api/src/models/rules/currency-cluster-risk/base-currency-initial-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/base-currency-initial-investment.ts index dfc8fd5d0..f019aa38c 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/base-currency-initial-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/base-currency-initial-investment.ts @@ -1,9 +1,12 @@ +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'; -export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule { +export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule { public constructor(public exchangeRateDataService: ExchangeRateDataService) { super(exchangeRateDataService, { name: 'Initial Investment: Base Currency' @@ -13,13 +16,8 @@ export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule { public evaluate( aPositions: { [symbol: string]: PortfolioPosition }, aFees: number, - aRuleSettingsMap?: { - [key: string]: any; - } + ruleSettings: Settings ) { - const ruleSettings = - aRuleSettingsMap[CurrencyClusterRiskBaseCurrencyInitialInvestment.name]; - const positionsGroupedByCurrency = this.groupPositionsByAttribute( aPositions, 'currency', @@ -62,4 +60,15 @@ export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule { value: true }; } + + public getSettings(aUserSettings: UserSettings): Settings { + return { + baseCurrency: aUserSettings.baseCurrency, + isActive: true + }; + } +} + +interface Settings extends RuleSettings { + baseCurrency: Currency; } diff --git a/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts index 305b5e59e..330d3ee31 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts @@ -2,8 +2,11 @@ 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'; -export class CurrencyClusterRiskCurrentInvestment extends Rule { +export class CurrencyClusterRiskCurrentInvestment extends Rule { public constructor(public exchangeRateDataService: ExchangeRateDataService) { super(exchangeRateDataService, { name: 'Current Investment' @@ -13,13 +16,8 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule { public evaluate( aPositions: { [symbol: string]: PortfolioPosition }, aFees: number, - aRuleSettingsMap?: { - [key: string]: any; - } + ruleSettings: Settings ) { - const ruleSettings = - aRuleSettingsMap[CurrencyClusterRiskCurrentInvestment.name]; - const positionsGroupedByCurrency = this.groupPositionsByAttribute( aPositions, 'currency', @@ -61,4 +59,17 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule { value: true }; } + + public getSettings(aUserSettings: UserSettings): Settings { + return { + baseCurrency: aUserSettings.baseCurrency, + isActive: true, + threshold: 0.5 + }; + } +} + +interface Settings extends RuleSettings { + baseCurrency: Currency; + threshold: number; } diff --git a/apps/api/src/models/rules/currency-cluster-risk/initial-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/initial-investment.ts index 70778cfea..a86ead3e5 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/initial-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/initial-investment.ts @@ -1,9 +1,12 @@ +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'; -export class CurrencyClusterRiskInitialInvestment extends Rule { +export class CurrencyClusterRiskInitialInvestment extends Rule { public constructor(public exchangeRateDataService: ExchangeRateDataService) { super(exchangeRateDataService, { name: 'Initial Investment' @@ -13,13 +16,8 @@ export class CurrencyClusterRiskInitialInvestment extends Rule { public evaluate( aPositions: { [symbol: string]: PortfolioPosition }, aFees: number, - aRuleSettingsMap?: { - [key: string]: any; - } + ruleSettings: Settings ) { - const ruleSettings = - aRuleSettingsMap[CurrencyClusterRiskInitialInvestment.name]; - const positionsGroupedByCurrency = this.groupPositionsByAttribute( aPositions, 'currency', @@ -61,4 +59,17 @@ export class CurrencyClusterRiskInitialInvestment extends Rule { value: true }; } + + public getSettings(aUserSettings: UserSettings): Settings { + return { + baseCurrency: aUserSettings.baseCurrency, + isActive: true, + threshold: 0.5 + }; + } +} + +interface Settings extends RuleSettings { + baseCurrency: Currency; + threshold: number; } diff --git a/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts b/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts index 9d8bd1be2..e5e9a91ee 100644 --- a/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts +++ b/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts @@ -1,9 +1,12 @@ +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'; -export class FeeRatioInitialInvestment extends Rule { +export class FeeRatioInitialInvestment extends Rule { public constructor(public exchangeRateDataService: ExchangeRateDataService) { super(exchangeRateDataService, { name: 'Initial Investment' @@ -13,12 +16,8 @@ export class FeeRatioInitialInvestment extends Rule { public evaluate( aPositions: { [symbol: string]: PortfolioPosition }, aFees: number, - aRuleSettingsMap?: { - [key: string]: any; - } + ruleSettings: Settings ) { - const ruleSettings = aRuleSettingsMap[FeeRatioInitialInvestment.name]; - const positionsGroupedByCurrency = this.groupPositionsByAttribute( aPositions, 'currency', @@ -50,4 +49,17 @@ export class FeeRatioInitialInvestment extends Rule { value: true }; } + + public getSettings(aUserSettings: UserSettings): Settings { + return { + baseCurrency: aUserSettings.baseCurrency, + isActive: true, + threshold: 0.01 + }; + } +} + +interface Settings extends RuleSettings { + baseCurrency: Currency; + threshold: number; } diff --git a/apps/api/src/services/rules.service.ts b/apps/api/src/services/rules.service.ts index 99e657481..655c172a3 100644 --- a/apps/api/src/services/rules.service.ts +++ b/apps/api/src/services/rules.service.ts @@ -1,78 +1,30 @@ import { Injectable } from '@nestjs/common'; - -import { Portfolio } from '../models/portfolio'; import { Rule } from '../models/rule'; -import { AccountClusterRiskCurrentInvestment } from '../models/rules/account-cluster-risk/current-investment'; -import { AccountClusterRiskInitialInvestment } from '../models/rules/account-cluster-risk/initial-investment'; -import { AccountClusterRiskSingleAccount } from '../models/rules/account-cluster-risk/single-account'; -import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '../models/rules/currency-cluster-risk/base-currency-current-investment'; -import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '../models/rules/currency-cluster-risk/base-currency-initial-investment'; -import { CurrencyClusterRiskCurrentInvestment } from '../models/rules/currency-cluster-risk/current-investment'; -import { CurrencyClusterRiskInitialInvestment } from '../models/rules/currency-cluster-risk/initial-investment'; -import { FeeRatioInitialInvestment } from '../models/rules/fees/fee-ratio-initial-investment'; +import { PortfolioPosition } from '@ghostfolio/common/interfaces'; +import { Currency } from '@prisma/client'; +import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; @Injectable() export class RulesService { public constructor() {} - public async evaluate( - aPortfolio: Portfolio, - aRules: Rule[], - aUserSettings: { baseCurrency: string } + public async evaluate( + details: { [p: string]: PortfolioPosition }, + fees: number, + aRules: Rule[], + aUserSettings: { baseCurrency: Currency } ) { - const defaultSettings = this.getDefaultRuleSettings(aUserSettings); - const details = await aPortfolio.getDetails(); - return aRules .filter((rule) => { - return defaultSettings[rule.constructor.name]?.isActive; + return rule.getSettings(aUserSettings)?.isActive; }) .map((rule) => { const evaluationResult = rule.evaluate( details, - aPortfolio.getFees(), - defaultSettings + fees, + rule.getSettings(aUserSettings) ); return { ...evaluationResult, name: rule.getName() }; }); } - - private getDefaultRuleSettings(aUserSettings: { baseCurrency: string }) { - return { - [AccountClusterRiskCurrentInvestment.name]: { - baseCurrency: aUserSettings.baseCurrency, - isActive: true, - threshold: 0.5 - }, - [AccountClusterRiskInitialInvestment.name]: { - baseCurrency: aUserSettings.baseCurrency, - isActive: true, - threshold: 0.5 - }, - [AccountClusterRiskSingleAccount.name]: { isActive: true }, - [CurrencyClusterRiskBaseCurrencyInitialInvestment.name]: { - baseCurrency: aUserSettings.baseCurrency, - isActive: true - }, - [CurrencyClusterRiskBaseCurrencyCurrentInvestment.name]: { - baseCurrency: aUserSettings.baseCurrency, - isActive: true - }, - [CurrencyClusterRiskCurrentInvestment.name]: { - baseCurrency: aUserSettings.baseCurrency, - isActive: true, - threshold: 0.5 - }, - [CurrencyClusterRiskInitialInvestment.name]: { - baseCurrency: aUserSettings.baseCurrency, - isActive: true, - threshold: 0.5 - }, - [FeeRatioInitialInvestment.name]: { - baseCurrency: aUserSettings.baseCurrency, - isActive: true, - threshold: 0.01 - } - }; - } }