diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index d5c660479..295577ead 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -621,30 +621,26 @@ export abstract class PortfolioCalculator { {} ); - const totalInvestment = await Object.keys(holdings[endString]).reduce( - (sum, holding) => { - if (!holdings[endString][holding].toNumber()) { - return sum; - } - const symbol = marketMap.values.find((m) => m.symbol === holding); + return Object.keys(holdings[endString]).reduce((sum, holding) => { + if (!holdings[endString][holding].toNumber()) { + return sum; + } + const symbol = marketMap.values.find((m) => m.symbol === holding); - if (symbol?.marketPrice === undefined) { - Logger.warn( - `Missing historical market data for ${holding} (${end})`, - 'PortfolioCalculator' - ); - return sum; - } else { - const symbolCurrency = this.getCurrency(holding); - const price = new Big(currencyRates[symbolCurrency]).mul( - symbol.marketPrice - ); - return sum.plus(new Big(price).mul(holdings[endString][holding])); - } - }, - new Big(0) - ); - return totalInvestment; + if (symbol?.marketPrice === undefined) { + Logger.warn( + `Missing historical market data for ${holding} (${end})`, + 'PortfolioCalculator' + ); + return sum; + } else { + const symbolCurrency = this.getCurrency(holding); + const price = new Big(currencyRates[symbolCurrency]).mul( + symbol.marketPrice + ); + return sum.plus(new Big(price).mul(holdings[endString][holding])); + } + }, new Big(0)); } @LogPerformance @@ -836,42 +832,6 @@ export abstract class PortfolioCalculator { }; } - private calculateTimeWeightedPerformance( - lastTimeWeightedPerformancePercentage: number, - historicalDataItem: HistoricalDataItem, - lastTimeWeightedPerformancePercentageWithCurrencyEffect: number, - timeWeightedPerformanceInPercentage: number, - timeWeightedPerformanceInPercentageWithCurrencyEffect: number - ): { - timeWeightedPerformanceInPercentage: number; - timeWeightedPerformanceInPercentageWithCurrencyEffect: number; - lastTimeWeightedPerformancePercentage: number; - lastTimeWeightedPerformancePercentageWithCurrencyEffect: number; - } { - timeWeightedPerformanceInPercentage = lastTimeWeightedPerformancePercentage - ? (1 + timeWeightedPerformanceInPercentage) * - ((1 + historicalDataItem.timeWeightedPerformanceInPercentage) / - (1 + lastTimeWeightedPerformancePercentage)) - - 1 - : 0; - timeWeightedPerformanceInPercentageWithCurrencyEffect = - lastTimeWeightedPerformancePercentageWithCurrencyEffect - ? (1 + timeWeightedPerformanceInPercentageWithCurrencyEffect) * - ((1 + - historicalDataItem.timeWeightedPerformanceInPercentageWithCurrencyEffect) / - (1 + lastTimeWeightedPerformancePercentageWithCurrencyEffect)) - - 1 - : 0; - return { - timeWeightedPerformanceInPercentage, - timeWeightedPerformanceInPercentageWithCurrencyEffect, - lastTimeWeightedPerformancePercentage: - historicalDataItem.timeWeightedPerformanceInPercentage, - lastTimeWeightedPerformancePercentageWithCurrencyEffect: - historicalDataItem.timeWeightedPerformanceInPercentageWithCurrencyEffect - }; - } - @LogPerformance protected getHistoricalDataItems(accumulatedValuesByDate: { [date: string]: { @@ -1375,6 +1335,41 @@ export abstract class PortfolioCalculator { return this.snapshot.totalValuablesWithCurrencyEffect; } + private calculateTimeWeightedPerformance( + lastTimeWeightedPerformancePercentage: number, + historicalDataItem: HistoricalDataItem, + lastTimeWeightedPerformancePercentageWithCurrencyEffect: number, + timeWeightedPerformanceInPercentage: number, + timeWeightedPerformanceInPercentageWithCurrencyEffect: number + ): { + timeWeightedPerformanceInPercentage: number; + timeWeightedPerformanceInPercentageWithCurrencyEffect: number; + lastTimeWeightedPerformancePercentage: number; + lastTimeWeightedPerformancePercentageWithCurrencyEffect: number; + } { + timeWeightedPerformanceInPercentage = lastTimeWeightedPerformancePercentage + ? (1 + timeWeightedPerformanceInPercentage) * + ((1 + historicalDataItem.timeWeightedPerformanceInPercentage) / + (1 + lastTimeWeightedPerformancePercentage)) - + 1 + : 0; + timeWeightedPerformanceInPercentageWithCurrencyEffect = + lastTimeWeightedPerformancePercentageWithCurrencyEffect + ? (1 + timeWeightedPerformanceInPercentageWithCurrencyEffect) * + ((1 + + historicalDataItem.timeWeightedPerformanceInPercentageWithCurrencyEffect) / + (1 + lastTimeWeightedPerformancePercentageWithCurrencyEffect)) - + 1 + : 0; + return { + timeWeightedPerformanceInPercentage, + timeWeightedPerformanceInPercentageWithCurrencyEffect, + lastTimeWeightedPerformancePercentage: + historicalDataItem.timeWeightedPerformanceInPercentage, + lastTimeWeightedPerformancePercentageWithCurrencyEffect: + historicalDataItem.timeWeightedPerformanceInPercentageWithCurrencyEffect + }; + } private calculateHoldings( investmentByDate: { [date: string]: PortfolioOrder[] }, @@ -1511,7 +1506,9 @@ export abstract class PortfolioCalculator { totalNetPerformanceValue: new Big(0), totalNetPerformanceValueWithCurrencyEffect: new Big(0), totalTimeWeightedInvestmentValue: new Big(0), - totalTimeWeightedInvestmentValueWithCurrencyEffect: new Big(0) + totalTimeWeightedInvestmentValueWithCurrencyEffect: new Big(0), + totalCurrentValue: new Big(0), + totalCurrentValueWithCurrencyEffect: new Big(0) }; const timeWeightedPerformanceCurrentPeriod = this.divideByOrZero( @@ -1519,7 +1516,7 @@ export abstract class PortfolioCalculator { totalNetPerformanceValue .minus(previousValues.totalNetPerformanceValue) .div(div), - previousValues.totalTimeWeightedInvestmentValue + previousValues.totalCurrentValue ); const timeWeightedPerformanceCurrentPeriodWithCurrencyEffect = this.divideByOrZero( @@ -1527,7 +1524,7 @@ export abstract class PortfolioCalculator { totalNetPerformanceValueWithCurrencyEffect .minus(previousValues.totalNetPerformanceValueWithCurrencyEffect) .div(div), - previousValues.totalTimeWeightedInvestmentValueWithCurrencyEffect + previousValues.totalCurrentValueWithCurrencyEffect ); const timeWeightedPerformanceInPercentage = new Big(1) diff --git a/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator-symbolmetrics-helper.ts b/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator-symbolmetrics-helper.ts index 656988f63..8d02dacc4 100644 --- a/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator-symbolmetrics-helper.ts +++ b/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator-symbolmetrics-helper.ts @@ -1,4 +1,3 @@ -import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { SymbolMetrics } from '@ghostfolio/common/interfaces'; @@ -28,7 +27,6 @@ export class RoiPortfolioCalculatorSymbolMetricsHelper { this.chartDates = chartDates; } - @LogPerformance public calculateNetPerformanceByDateRange( start: Date, symbolMetricsHelper: PortfolioCalculatorSymbolMetricsHelperObject @@ -61,21 +59,28 @@ export class RoiPortfolioCalculatorSymbolMetricsHelper { new Big(0)) ) ?? new Big(0); + let investmentBasis = + symbolMetricsHelper.symbolMetrics.currentValuesWithCurrencyEffect[ + rangeStartDateString + ]; + + if ( + !symbolMetricsHelper.symbolMetrics.currentValuesWithCurrencyEffect[ + rangeStartDateString + ]?.gt(0) + ) { + investmentBasis = + symbolMetricsHelper.symbolMetrics + .timeWeightedInvestmentValuesWithCurrencyEffect[rangeEndDateString]; + } + symbolMetricsHelper.symbolMetrics.netPerformancePercentageWithCurrencyEffectMap[ dateRange - ] = - symbolMetricsHelper.symbolMetrics.timeWeightedInvestmentValuesWithCurrencyEffect[ - rangeEndDateString - ]?.gt(0) - ? symbolMetricsHelper.symbolMetrics.netPerformanceWithCurrencyEffectMap[ - dateRange - ].div( - symbolMetricsHelper.symbolMetrics - .timeWeightedInvestmentValuesWithCurrencyEffect[ - rangeEndDateString - ] - ) - : new Big(0); + ] = investmentBasis.gt(0) + ? symbolMetricsHelper.symbolMetrics.netPerformanceWithCurrencyEffectMap[ + dateRange + ].div(investmentBasis) + : new Big(0); } } diff --git a/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts index dc3051909..faef77712 100644 --- a/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts @@ -79,7 +79,6 @@ export class RoiPortfolioCalculator extends PortfolioCalculator { }; } - @LogPerformance protected getSymbolMetrics({ chartDateMap, dataSource, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 96c89fc63..d93767ab2 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1356,145 +1356,6 @@ export class PortfolioService { await this.orderService.assignTags({ dataSource, symbol, tags, userId }); } - private getAggregatedMarkets(holdings: Record): { - markets: PortfolioDetails['markets']; - marketsAdvanced: PortfolioDetails['marketsAdvanced']; - } { - const markets: PortfolioDetails['markets'] = { - [UNKNOWN_KEY]: { - id: UNKNOWN_KEY, - valueInBaseCurrency: 0, - valueInPercentage: 0 - }, - developedMarkets: { - id: 'developedMarkets', - valueInBaseCurrency: 0, - valueInPercentage: 0 - }, - emergingMarkets: { - id: 'emergingMarkets', - valueInBaseCurrency: 0, - valueInPercentage: 0 - }, - otherMarkets: { - id: 'otherMarkets', - valueInBaseCurrency: 0, - valueInPercentage: 0 - } - }; - - const marketsAdvanced: PortfolioDetails['marketsAdvanced'] = { - [UNKNOWN_KEY]: { - id: UNKNOWN_KEY, - valueInBaseCurrency: 0, - valueInPercentage: 0 - }, - asiaPacific: { - id: 'asiaPacific', - valueInBaseCurrency: 0, - valueInPercentage: 0 - }, - emergingMarkets: { - id: 'emergingMarkets', - valueInBaseCurrency: 0, - valueInPercentage: 0 - }, - europe: { - id: 'europe', - valueInBaseCurrency: 0, - valueInPercentage: 0 - }, - japan: { - id: 'japan', - valueInBaseCurrency: 0, - valueInPercentage: 0 - }, - northAmerica: { - id: 'northAmerica', - valueInBaseCurrency: 0, - valueInPercentage: 0 - }, - otherMarkets: { - id: 'otherMarkets', - valueInBaseCurrency: 0, - valueInPercentage: 0 - } - }; - - for (const [, position] of Object.entries(holdings)) { - const value = position.valueInBaseCurrency; - - if (position.assetClass !== AssetClass.LIQUIDITY) { - if (position.countries.length > 0) { - markets.developedMarkets.valueInBaseCurrency += - position.markets.developedMarkets * value; - markets.emergingMarkets.valueInBaseCurrency += - position.markets.emergingMarkets * value; - markets.otherMarkets.valueInBaseCurrency += - position.markets.otherMarkets * value; - - marketsAdvanced.asiaPacific.valueInBaseCurrency += - position.marketsAdvanced.asiaPacific * value; - marketsAdvanced.emergingMarkets.valueInBaseCurrency += - position.marketsAdvanced.emergingMarkets * value; - marketsAdvanced.europe.valueInBaseCurrency += - position.marketsAdvanced.europe * value; - marketsAdvanced.japan.valueInBaseCurrency += - position.marketsAdvanced.japan * value; - marketsAdvanced.northAmerica.valueInBaseCurrency += - position.marketsAdvanced.northAmerica * value; - marketsAdvanced.otherMarkets.valueInBaseCurrency += - position.marketsAdvanced.otherMarkets * value; - } else { - markets[UNKNOWN_KEY].valueInBaseCurrency += value; - marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency += value; - } - } - } - - const marketsTotalInBaseCurrency = getSum( - Object.values(markets).map(({ valueInBaseCurrency }) => { - return new Big(valueInBaseCurrency); - }) - ).toNumber(); - - markets.developedMarkets.valueInPercentage = - markets.developedMarkets.valueInBaseCurrency / marketsTotalInBaseCurrency; - markets.emergingMarkets.valueInPercentage = - markets.emergingMarkets.valueInBaseCurrency / marketsTotalInBaseCurrency; - markets.otherMarkets.valueInPercentage = - markets.otherMarkets.valueInBaseCurrency / marketsTotalInBaseCurrency; - markets[UNKNOWN_KEY].valueInPercentage = - markets[UNKNOWN_KEY].valueInBaseCurrency / marketsTotalInBaseCurrency; - - const marketsAdvancedTotal = - marketsAdvanced.asiaPacific.valueInBaseCurrency + - marketsAdvanced.emergingMarkets.valueInBaseCurrency + - marketsAdvanced.europe.valueInBaseCurrency + - marketsAdvanced.japan.valueInBaseCurrency + - marketsAdvanced.northAmerica.valueInBaseCurrency + - marketsAdvanced.otherMarkets.valueInBaseCurrency + - marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency; - - marketsAdvanced.asiaPacific.valueInPercentage = - marketsAdvanced.asiaPacific.valueInBaseCurrency / marketsAdvancedTotal; - marketsAdvanced.emergingMarkets.valueInPercentage = - marketsAdvanced.emergingMarkets.valueInBaseCurrency / - marketsAdvancedTotal; - marketsAdvanced.europe.valueInPercentage = - marketsAdvanced.europe.valueInBaseCurrency / marketsAdvancedTotal; - marketsAdvanced.japan.valueInPercentage = - marketsAdvanced.japan.valueInBaseCurrency / marketsAdvancedTotal; - marketsAdvanced.northAmerica.valueInPercentage = - marketsAdvanced.northAmerica.valueInBaseCurrency / marketsAdvancedTotal; - marketsAdvanced.otherMarkets.valueInPercentage = - marketsAdvanced.otherMarkets.valueInBaseCurrency / marketsAdvancedTotal; - marketsAdvanced[UNKNOWN_KEY].valueInPercentage = - marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency / marketsAdvancedTotal; - - return { markets, marketsAdvanced }; - } - @LogPerformance private async getCashPositions({ cashDetails, @@ -1546,68 +1407,6 @@ export class PortfolioService { return cashPositions; } - private getDividendsByGroup({ - dividends, - groupBy - }: { - dividends: InvestmentItem[]; - groupBy: GroupBy; - }): InvestmentItem[] { - if (dividends.length === 0) { - return []; - } - - const dividendsByGroup: InvestmentItem[] = []; - let currentDate: Date; - let investmentByGroup = new Big(0); - - for (const [index, dividend] of dividends.entries()) { - if ( - isSameYear(parseDate(dividend.date), currentDate) && - (groupBy === 'year' || - isSameMonth(parseDate(dividend.date), currentDate)) - ) { - // Same group: Add up dividends - - investmentByGroup = investmentByGroup.plus(dividend.investment); - } else { - // New group: Store previous group and reset - - if (currentDate) { - dividendsByGroup.push({ - date: format( - set(currentDate, { - date: 1, - month: groupBy === 'year' ? 0 : currentDate.getMonth() - }), - DATE_FORMAT - ), - investment: investmentByGroup.toNumber() - }); - } - - currentDate = parseDate(dividend.date); - investmentByGroup = new Big(dividend.investment); - } - - if (index === dividends.length - 1) { - // Store current month (latest order) - dividendsByGroup.push({ - date: format( - set(currentDate, { - date: 1, - month: groupBy === 'year' ? 0 : currentDate.getMonth() - }), - DATE_FORMAT - ), - investment: investmentByGroup.toNumber() - }); - } - } - - return dividendsByGroup; - } - @LogPerformance private getEmergencyFundHoldingsValueInBaseCurrency({ holdings @@ -1634,145 +1433,23 @@ export class PortfolioService { return valueInBaseCurrencyOfEmergencyFundHoldings.toNumber(); } - private getInitialCashPosition({ - balance, - currency - }: { - balance: number; - currency: string; - }): PortfolioPosition { - return { - currency, - allocationInPercentage: 0, - assetClass: AssetClass.LIQUIDITY, - assetSubClass: AssetSubClass.CASH, - countries: [], - dataSource: undefined, - dateOfFirstActivity: undefined, - dividend: 0, - grossPerformance: 0, - grossPerformancePercent: 0, - grossPerformancePercentWithCurrencyEffect: 0, - grossPerformanceWithCurrencyEffect: 0, - holdings: [], - investment: balance, - marketPrice: 0, - name: currency, - netPerformance: 0, - netPerformancePercent: 0, - netPerformancePercentWithCurrencyEffect: 0, - netPerformanceWithCurrencyEffect: 0, - quantity: 0, - sectors: [], - symbol: currency, - tags: [], - transactionCount: 0, - valueInBaseCurrency: balance - }; - } - - private getMarkets({ - assetProfile - }: { - assetProfile: EnhancedSymbolProfile; - }) { - const markets = { - [UNKNOWN_KEY]: 0, - developedMarkets: 0, - emergingMarkets: 0, - otherMarkets: 0 - }; - const marketsAdvanced = { - [UNKNOWN_KEY]: 0, - asiaPacific: 0, - emergingMarkets: 0, - europe: 0, - japan: 0, - northAmerica: 0, - otherMarkets: 0 - }; - - if (assetProfile.countries.length > 0) { - for (const country of assetProfile.countries) { - if (developedMarkets.includes(country.code)) { - markets.developedMarkets = new Big(markets.developedMarkets) - .plus(country.weight) - .toNumber(); - } else if (emergingMarkets.includes(country.code)) { - markets.emergingMarkets = new Big(markets.emergingMarkets) - .plus(country.weight) - .toNumber(); - } else { - markets.otherMarkets = new Big(markets.otherMarkets) - .plus(country.weight) - .toNumber(); - } - - if (country.code === 'JP') { - marketsAdvanced.japan = new Big(marketsAdvanced.japan) - .plus(country.weight) - .toNumber(); - } else if (country.code === 'CA' || country.code === 'US') { - marketsAdvanced.northAmerica = new Big(marketsAdvanced.northAmerica) - .plus(country.weight) - .toNumber(); - } else if (asiaPacificMarkets.includes(country.code)) { - marketsAdvanced.asiaPacific = new Big(marketsAdvanced.asiaPacific) - .plus(country.weight) - .toNumber(); - } else if (emergingMarkets.includes(country.code)) { - marketsAdvanced.emergingMarkets = new Big( - marketsAdvanced.emergingMarkets - ) - .plus(country.weight) - .toNumber(); - } else if (europeMarkets.includes(country.code)) { - marketsAdvanced.europe = new Big(marketsAdvanced.europe) - .plus(country.weight) - .toNumber(); - } else { - marketsAdvanced.otherMarkets = new Big(marketsAdvanced.otherMarkets) - .plus(country.weight) - .toNumber(); - } - } - } - - markets[UNKNOWN_KEY] = new Big(1) - .minus(markets.developedMarkets) - .minus(markets.emergingMarkets) - .minus(markets.otherMarkets) - .toNumber(); - - marketsAdvanced[UNKNOWN_KEY] = new Big(1) - .minus(marketsAdvanced.asiaPacific) - .minus(marketsAdvanced.emergingMarkets) - .minus(marketsAdvanced.europe) - .minus(marketsAdvanced.japan) - .minus(marketsAdvanced.northAmerica) - .minus(marketsAdvanced.otherMarkets) - .toNumber(); - - return { markets, marketsAdvanced }; - } - - @LogPerformance - private getReportStatistics( - evaluatedRules: PortfolioReportResponse['rules'] - ): PortfolioReportResponse['statistics'] { - const rulesActiveCount = Object.values(evaluatedRules) - .flat() - .filter((rule) => { - return rule?.isActive === true; - }).length; - - const rulesFulfilledCount = Object.values(evaluatedRules) - .flat() - .filter((rule) => { - return rule?.value === true; - }).length; - - return { rulesActiveCount, rulesFulfilledCount }; + @LogPerformance + private getReportStatistics( + evaluatedRules: PortfolioReportResponse['rules'] + ): PortfolioReportResponse['statistics'] { + const rulesActiveCount = Object.values(evaluatedRules) + .flat() + .filter((rule) => { + return rule?.isActive === true; + }).length; + + const rulesFulfilledCount = Object.values(evaluatedRules) + .flat() + .filter((rule) => { + return rule?.value === true; + }).length; + + return { rulesActiveCount, rulesFulfilledCount }; } @LogPerformance @@ -1981,69 +1658,6 @@ export class PortfolioService { }; } - @LogPerformance - private getSumOfActivityType({ - activities, - activityType, - userCurrency - }: { - activities: Activity[]; - activityType: ActivityType; - userCurrency: string; - }) { - return getSum( - activities - .filter(({ isDraft, type }) => { - return isDraft === false && type === activityType; - }) - .map(({ quantity, SymbolProfile, unitPrice }) => { - return new Big( - this.exchangeRateDataService.toCurrency( - new Big(quantity).mul(unitPrice).toNumber(), - SymbolProfile.currency, - userCurrency - ) - ); - }) - ); - } - - private getTotalEmergencyFund({ - emergencyFundHoldingsValueInBaseCurrency, - userSettings - }: { - emergencyFundHoldingsValueInBaseCurrency: number; - userSettings: UserSettings; - }) { - return new Big( - Math.max( - emergencyFundHoldingsValueInBaseCurrency, - userSettings?.emergencyFund ?? 0 - ) - ); - } - - private getUserCurrency(aUser?: UserWithSettings) { - return ( - aUser?.Settings?.settings.baseCurrency ?? - this.request.user?.Settings?.settings.baseCurrency ?? - DEFAULT_CURRENCY - ); - } - - private async getUserId(aImpersonationId: string, aUserId: string) { - const impersonationUserId = - await this.impersonationService.validateImpersonationId(aImpersonationId); - - return impersonationUserId || aUserId; - } - - private getUserPerformanceCalculationType( - aUser: UserWithSettings - ): PerformanceCalculationType { - return aUser?.Settings?.settings.performanceCalculationType; - } - @LogPerformance private async getValueOfAccountsAndPlatforms({ activities, @@ -2176,4 +1790,390 @@ export class PortfolioService { return { accounts, platforms }; } + + @LogPerformance + private getSumOfActivityType({ + activities, + activityType, + userCurrency + }: { + activities: Activity[]; + activityType: ActivityType; + userCurrency: string; + }) { + return getSum( + activities + .filter(({ isDraft, type }) => { + return isDraft === false && type === activityType; + }) + .map(({ quantity, SymbolProfile, unitPrice }) => { + return new Big( + this.exchangeRateDataService.toCurrency( + new Big(quantity).mul(unitPrice).toNumber(), + SymbolProfile.currency, + userCurrency + ) + ); + }) + ); + } + + private getInitialCashPosition({ + balance, + currency + }: { + balance: number; + currency: string; + }): PortfolioPosition { + return { + currency, + allocationInPercentage: 0, + assetClass: AssetClass.LIQUIDITY, + assetSubClass: AssetSubClass.CASH, + countries: [], + dataSource: undefined, + dateOfFirstActivity: undefined, + dividend: 0, + grossPerformance: 0, + grossPerformancePercent: 0, + grossPerformancePercentWithCurrencyEffect: 0, + grossPerformanceWithCurrencyEffect: 0, + holdings: [], + investment: balance, + marketPrice: 0, + name: currency, + netPerformance: 0, + netPerformancePercent: 0, + netPerformancePercentWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0, + quantity: 0, + sectors: [], + symbol: currency, + tags: [], + transactionCount: 0, + valueInBaseCurrency: balance + }; + } + + private getDividendsByGroup({ + dividends, + groupBy + }: { + dividends: InvestmentItem[]; + groupBy: GroupBy; + }): InvestmentItem[] { + if (dividends.length === 0) { + return []; + } + + const dividendsByGroup: InvestmentItem[] = []; + let currentDate: Date; + let investmentByGroup = new Big(0); + + for (const [index, dividend] of dividends.entries()) { + if ( + isSameYear(parseDate(dividend.date), currentDate) && + (groupBy === 'year' || + isSameMonth(parseDate(dividend.date), currentDate)) + ) { + // Same group: Add up dividends + + investmentByGroup = investmentByGroup.plus(dividend.investment); + } else { + // New group: Store previous group and reset + + if (currentDate) { + dividendsByGroup.push({ + date: format( + set(currentDate, { + date: 1, + month: groupBy === 'year' ? 0 : currentDate.getMonth() + }), + DATE_FORMAT + ), + investment: investmentByGroup.toNumber() + }); + } + + currentDate = parseDate(dividend.date); + investmentByGroup = new Big(dividend.investment); + } + + if (index === dividends.length - 1) { + // Store current month (latest order) + dividendsByGroup.push({ + date: format( + set(currentDate, { + date: 1, + month: groupBy === 'year' ? 0 : currentDate.getMonth() + }), + DATE_FORMAT + ), + investment: investmentByGroup.toNumber() + }); + } + } + + return dividendsByGroup; + } + + private getMarkets({ + assetProfile + }: { + assetProfile: EnhancedSymbolProfile; + }) { + const markets = { + [UNKNOWN_KEY]: 0, + developedMarkets: 0, + emergingMarkets: 0, + otherMarkets: 0 + }; + const marketsAdvanced = { + [UNKNOWN_KEY]: 0, + asiaPacific: 0, + emergingMarkets: 0, + europe: 0, + japan: 0, + northAmerica: 0, + otherMarkets: 0 + }; + + if (assetProfile.countries.length > 0) { + for (const country of assetProfile.countries) { + if (developedMarkets.includes(country.code)) { + markets.developedMarkets = new Big(markets.developedMarkets) + .plus(country.weight) + .toNumber(); + } else if (emergingMarkets.includes(country.code)) { + markets.emergingMarkets = new Big(markets.emergingMarkets) + .plus(country.weight) + .toNumber(); + } else { + markets.otherMarkets = new Big(markets.otherMarkets) + .plus(country.weight) + .toNumber(); + } + + if (country.code === 'JP') { + marketsAdvanced.japan = new Big(marketsAdvanced.japan) + .plus(country.weight) + .toNumber(); + } else if (country.code === 'CA' || country.code === 'US') { + marketsAdvanced.northAmerica = new Big(marketsAdvanced.northAmerica) + .plus(country.weight) + .toNumber(); + } else if (asiaPacificMarkets.includes(country.code)) { + marketsAdvanced.asiaPacific = new Big(marketsAdvanced.asiaPacific) + .plus(country.weight) + .toNumber(); + } else if (emergingMarkets.includes(country.code)) { + marketsAdvanced.emergingMarkets = new Big( + marketsAdvanced.emergingMarkets + ) + .plus(country.weight) + .toNumber(); + } else if (europeMarkets.includes(country.code)) { + marketsAdvanced.europe = new Big(marketsAdvanced.europe) + .plus(country.weight) + .toNumber(); + } else { + marketsAdvanced.otherMarkets = new Big(marketsAdvanced.otherMarkets) + .plus(country.weight) + .toNumber(); + } + } + } + + markets[UNKNOWN_KEY] = new Big(1) + .minus(markets.developedMarkets) + .minus(markets.emergingMarkets) + .minus(markets.otherMarkets) + .toNumber(); + + marketsAdvanced[UNKNOWN_KEY] = new Big(1) + .minus(marketsAdvanced.asiaPacific) + .minus(marketsAdvanced.emergingMarkets) + .minus(marketsAdvanced.europe) + .minus(marketsAdvanced.japan) + .minus(marketsAdvanced.northAmerica) + .minus(marketsAdvanced.otherMarkets) + .toNumber(); + + return { markets, marketsAdvanced }; + } + + private getTotalEmergencyFund({ + emergencyFundHoldingsValueInBaseCurrency, + userSettings + }: { + emergencyFundHoldingsValueInBaseCurrency: number; + userSettings: UserSettings; + }) { + return new Big( + Math.max( + emergencyFundHoldingsValueInBaseCurrency, + userSettings?.emergencyFund ?? 0 + ) + ); + } + + private getUserCurrency(aUser?: UserWithSettings) { + return ( + aUser?.Settings?.settings.baseCurrency ?? + this.request.user?.Settings?.settings.baseCurrency ?? + DEFAULT_CURRENCY + ); + } + + private async getUserId(aImpersonationId: string, aUserId: string) { + const impersonationUserId = + await this.impersonationService.validateImpersonationId(aImpersonationId); + + return impersonationUserId || aUserId; + } + + private getUserPerformanceCalculationType( + aUser: UserWithSettings + ): PerformanceCalculationType { + return aUser?.Settings?.settings.performanceCalculationType; + } + + private getAggregatedMarkets(holdings: Record): { + markets: PortfolioDetails['markets']; + marketsAdvanced: PortfolioDetails['marketsAdvanced']; + } { + const markets: PortfolioDetails['markets'] = { + [UNKNOWN_KEY]: { + id: UNKNOWN_KEY, + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + developedMarkets: { + id: 'developedMarkets', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + emergingMarkets: { + id: 'emergingMarkets', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + otherMarkets: { + id: 'otherMarkets', + valueInBaseCurrency: 0, + valueInPercentage: 0 + } + }; + + const marketsAdvanced: PortfolioDetails['marketsAdvanced'] = { + [UNKNOWN_KEY]: { + id: UNKNOWN_KEY, + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + asiaPacific: { + id: 'asiaPacific', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + emergingMarkets: { + id: 'emergingMarkets', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + europe: { + id: 'europe', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + japan: { + id: 'japan', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + northAmerica: { + id: 'northAmerica', + valueInBaseCurrency: 0, + valueInPercentage: 0 + }, + otherMarkets: { + id: 'otherMarkets', + valueInBaseCurrency: 0, + valueInPercentage: 0 + } + }; + + for (const [, position] of Object.entries(holdings)) { + const value = position.valueInBaseCurrency; + + if (position.assetClass !== AssetClass.LIQUIDITY) { + if (position.countries.length > 0) { + markets.developedMarkets.valueInBaseCurrency += + position.markets.developedMarkets * value; + markets.emergingMarkets.valueInBaseCurrency += + position.markets.emergingMarkets * value; + markets.otherMarkets.valueInBaseCurrency += + position.markets.otherMarkets * value; + + marketsAdvanced.asiaPacific.valueInBaseCurrency += + position.marketsAdvanced.asiaPacific * value; + marketsAdvanced.emergingMarkets.valueInBaseCurrency += + position.marketsAdvanced.emergingMarkets * value; + marketsAdvanced.europe.valueInBaseCurrency += + position.marketsAdvanced.europe * value; + marketsAdvanced.japan.valueInBaseCurrency += + position.marketsAdvanced.japan * value; + marketsAdvanced.northAmerica.valueInBaseCurrency += + position.marketsAdvanced.northAmerica * value; + marketsAdvanced.otherMarkets.valueInBaseCurrency += + position.marketsAdvanced.otherMarkets * value; + } else { + markets[UNKNOWN_KEY].valueInBaseCurrency += value; + marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency += value; + } + } + } + + const marketsTotalInBaseCurrency = getSum( + Object.values(markets).map(({ valueInBaseCurrency }) => { + return new Big(valueInBaseCurrency); + }) + ).toNumber(); + + markets.developedMarkets.valueInPercentage = + markets.developedMarkets.valueInBaseCurrency / marketsTotalInBaseCurrency; + markets.emergingMarkets.valueInPercentage = + markets.emergingMarkets.valueInBaseCurrency / marketsTotalInBaseCurrency; + markets.otherMarkets.valueInPercentage = + markets.otherMarkets.valueInBaseCurrency / marketsTotalInBaseCurrency; + markets[UNKNOWN_KEY].valueInPercentage = + markets[UNKNOWN_KEY].valueInBaseCurrency / marketsTotalInBaseCurrency; + + const marketsAdvancedTotal = + marketsAdvanced.asiaPacific.valueInBaseCurrency + + marketsAdvanced.emergingMarkets.valueInBaseCurrency + + marketsAdvanced.europe.valueInBaseCurrency + + marketsAdvanced.japan.valueInBaseCurrency + + marketsAdvanced.northAmerica.valueInBaseCurrency + + marketsAdvanced.otherMarkets.valueInBaseCurrency + + marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency; + + marketsAdvanced.asiaPacific.valueInPercentage = + marketsAdvanced.asiaPacific.valueInBaseCurrency / marketsAdvancedTotal; + marketsAdvanced.emergingMarkets.valueInPercentage = + marketsAdvanced.emergingMarkets.valueInBaseCurrency / + marketsAdvancedTotal; + marketsAdvanced.europe.valueInPercentage = + marketsAdvanced.europe.valueInBaseCurrency / marketsAdvancedTotal; + marketsAdvanced.japan.valueInPercentage = + marketsAdvanced.japan.valueInBaseCurrency / marketsAdvancedTotal; + marketsAdvanced.northAmerica.valueInPercentage = + marketsAdvanced.northAmerica.valueInBaseCurrency / marketsAdvancedTotal; + marketsAdvanced.otherMarkets.valueInPercentage = + marketsAdvanced.otherMarkets.valueInBaseCurrency / marketsAdvancedTotal; + marketsAdvanced[UNKNOWN_KEY].valueInPercentage = + marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency / marketsAdvancedTotal; + + return { markets, marketsAdvanced }; + } }