diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 4b0e752c0..bb24af515 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -136,7 +136,7 @@ export abstract class PortfolioCalculator { }); this.redisCacheService = redisCacheService; - this.useCache = false; // TODO: useCache + this.useCache = useCache; this.userId = userId; const { endDate, startDate } = getIntervalFromDateRange( @@ -394,9 +394,11 @@ export abstract class PortfolioCalculator { netPerformance, netPerformancePercentage, netPerformancePercentageWithCurrencyEffect, + netPerformancePercentageWithCurrencyEffectMap, netPerformanceValues, netPerformanceValuesWithCurrencyEffect, netPerformanceWithCurrencyEffect, + netPerformanceWithCurrencyEffectMap, timeWeightedInvestment, timeWeightedInvestmentValues, timeWeightedInvestmentValuesWithCurrencyEffect, @@ -468,9 +470,15 @@ export abstract class PortfolioCalculator { netPerformancePercentageWithCurrencyEffect: !hasErrors ? (netPerformancePercentageWithCurrencyEffect ?? null) : null, + netPerformancePercentageWithCurrencyEffectMap: !hasErrors + ? (netPerformancePercentageWithCurrencyEffectMap ?? null) + : null, netPerformanceWithCurrencyEffect: !hasErrors ? (netPerformanceWithCurrencyEffect ?? null) : null, + netPerformanceWithCurrencyEffectMap: !hasErrors + ? (netPerformanceWithCurrencyEffectMap ?? null) + : null, quantity: item.quantity, symbol: item.symbol, tags: item.tags, diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index 53a4ec826..b4443d556 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -1,12 +1,14 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; +import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, SymbolMetrics } from '@ghostfolio/common/interfaces'; import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; +import { DateRange } from '@ghostfolio/common/types'; import { Logger } from '@nestjs/common'; import { Big } from 'big.js'; @@ -14,6 +16,7 @@ import { addDays, addMilliseconds, differenceInDays, + eachDayOfInterval, format, isBefore } from 'date-fns'; @@ -236,9 +239,11 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { netPerformance: new Big(0), netPerformancePercentage: new Big(0), netPerformancePercentageWithCurrencyEffect: new Big(0), + netPerformancePercentageWithCurrencyEffectMap: {}, netPerformanceValues: {}, netPerformanceValuesWithCurrencyEffect: {}, netPerformanceWithCurrencyEffect: new Big(0), + netPerformanceWithCurrencyEffectMap: {}, timeWeightedInvestment: new Big(0), timeWeightedInvestmentValues: {}, timeWeightedInvestmentValuesWithCurrencyEffect: {}, @@ -286,6 +291,8 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { netPerformance: new Big(0), netPerformancePercentage: new Big(0), netPerformancePercentageWithCurrencyEffect: new Big(0), + netPerformancePercentageWithCurrencyEffectMap: {}, + netPerformanceWithCurrencyEffectMap: {}, netPerformanceValues: {}, netPerformanceValuesWithCurrencyEffect: {}, netPerformanceWithCurrencyEffect: new Big(0), @@ -841,6 +848,87 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { ) : new Big(0); + const netPerformancePercentageWithCurrencyEffectMap: { + [key: DateRange]: Big; + } = {}; + + const netPerformanceWithCurrencyEffectMap: { + [key: DateRange]: Big; + } = {}; + + for (const dateRange of [ + '1d', + '1y', + '5y', + 'max', + 'mtd', + 'wtd', + 'ytd' + // TODO: '2024', '2023', '2022', etc. + ]) { + // TODO: getIntervalFromDateRange(dateRange, start) + let { endDate, startDate } = getIntervalFromDateRange(dateRange); + + if (isBefore(startDate, start)) { + startDate = addDays(start, 1); + } + + let average = new Big(0); + + const currentValuesAtStartDateWithCurrencyEffect = + currentValuesWithCurrencyEffect[format(startDate, DATE_FORMAT)]; + + const investmentValuesAccumulatedAtStartDateWithCurrencyEffect = + investmentValuesAccumulatedWithCurrencyEffect[ + format(startDate, DATE_FORMAT) + ]; + + // TODO: Rename? + const grossPerformanceAtStartDateWithCurrencyEffect2 = + currentValuesAtStartDateWithCurrencyEffect.minus( + investmentValuesAccumulatedAtStartDateWithCurrencyEffect + ); + + const dates = eachDayOfInterval({ + end: endDate, + start: startDate + }).map((date) => { + return format(date, DATE_FORMAT); + }); + + let dayCount = 0; + + for (const date of dates) { + if ( + investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big + ) { + average = average.add( + investmentValuesAccumulatedWithCurrencyEffect[date].add( + grossPerformanceAtStartDateWithCurrencyEffect2 + ) + ); + + dayCount++; + } + } + + average = average.div(dayCount); + + netPerformanceWithCurrencyEffectMap[dateRange] = average.gt(0) + ? netPerformanceValuesWithCurrencyEffect[ + format(endDate, DATE_FORMAT) + ].minus( + netPerformanceValuesWithCurrencyEffect[ + format(startDate, DATE_FORMAT) + ] + ) + : new Big(0); + + netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0) + ? netPerformanceWithCurrencyEffectMap[dateRange].div(average) + : new Big(0); + } + if (PortfolioCalculator.ENABLE_LOGGING) { console.log( ` @@ -893,8 +981,10 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { investmentValuesWithCurrencyEffect, netPerformancePercentage, netPerformancePercentageWithCurrencyEffect, + netPerformancePercentageWithCurrencyEffectMap, netPerformanceValues, netPerformanceValuesWithCurrencyEffect, + netPerformanceWithCurrencyEffectMap, timeWeightedInvestmentValues, timeWeightedInvestmentValuesWithCurrencyEffect, totalAccountBalanceInBaseCurrency, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b0e07d0b2..178870f5f 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -346,7 +346,7 @@ export class PortfolioService { userId, calculationType: PerformanceCalculationType.TWR, currency: userCurrency, - hasFilters: true, // disable cache + hasFilters: filters?.length > 0, // TODO isExperimentalFeatures: this.request.user?.Settings.settings.isExperimentalFeatures }); @@ -412,10 +412,8 @@ export class PortfolioService { console.timeEnd('-- PortfolioService.getDetails - 3'); console.time('-- PortfolioService.getDetails - 4'); - const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.getQuotes({ user, items: dataGatheringItems }), - this.symbolProfileService.getSymbolProfiles(dataGatheringItems) - ]); + const symbolProfiles = + await this.symbolProfileService.getSymbolProfiles(dataGatheringItems); const symbolProfileMap: { [symbol: string]: EnhancedSymbolProfile } = {}; for (const symbolProfile of symbolProfiles) { @@ -442,8 +440,10 @@ export class PortfolioService { marketPrice, netPerformance, netPerformancePercentage, - netPerformancePercentageWithCurrencyEffect, - netPerformanceWithCurrencyEffect, + netPerformancePercentageWithCurrencyEffect, // TODO: Remove? + netPerformancePercentageWithCurrencyEffectMap, + netPerformanceWithCurrencyEffect, // TODO: Remove? + netPerformanceWithCurrencyEffectMap, quantity, symbol, tags, @@ -463,7 +463,6 @@ export class PortfolioService { } const assetProfile = symbolProfileMap[symbol]; - const dataProviderResponse = dataProviderResponses[symbol]; let markets: PortfolioPosition['markets']; let marketsAdvanced: PortfolioPosition['marketsAdvanced']; @@ -510,14 +509,15 @@ export class PortfolioService { } ), investment: investment.toNumber(), - marketState: dataProviderResponse?.marketState ?? 'delayed', name: assetProfile.name, netPerformance: netPerformance?.toNumber() ?? 0, netPerformancePercent: netPerformancePercentage?.toNumber() ?? 0, netPerformancePercentWithCurrencyEffect: - netPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0, + netPerformancePercentageWithCurrencyEffectMap?.[ + dateRange + ]?.toNumber() ?? 0, netPerformanceWithCurrencyEffect: - netPerformanceWithCurrencyEffect?.toNumber() ?? 0, + netPerformanceWithCurrencyEffectMap?.[dateRange]?.toNumber() ?? 0, quantity: quantity.toNumber(), sectors: assetProfile.sectors, url: assetProfile.url, @@ -1489,7 +1489,6 @@ export class PortfolioService { holdings: [], investment: balance, marketPrice: 0, - marketState: 'open', name: currency, netPerformance: 0, netPerformancePercent: 0, diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 7aa1dbbe8..225af56eb 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -1,7 +1,6 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { environment } from '@ghostfolio/api/environments/environment'; -import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; @@ -27,7 +26,6 @@ import { import { UserWithSettings } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; -import { EventEmitter2 } from '@nestjs/event-emitter'; import { Prisma, Role, User } from '@prisma/client'; import { differenceInDays } from 'date-fns'; import { sortBy, without } from 'lodash'; @@ -40,7 +38,6 @@ export class UserService { public constructor( private readonly configurationService: ConfigurationService, - private readonly eventEmitter: EventEmitter2, private readonly orderService: OrderService, private readonly prismaService: PrismaService, private readonly propertyService: PropertyService, @@ -444,13 +441,6 @@ export class UserService { } }); - this.eventEmitter.emit( - PortfolioChangedEvent.getName(), - new PortfolioChangedEvent({ - userId - }) - ); - return settings; } diff --git a/libs/common/src/lib/class-transformer.ts b/libs/common/src/lib/class-transformer.ts index bd9db22da..328e2bf9e 100644 --- a/libs/common/src/lib/class-transformer.ts +++ b/libs/common/src/lib/class-transformer.ts @@ -1,5 +1,21 @@ import { Big } from 'big.js'; +export function transformToMapOfBig({ + value +}: { + value: { [key: string]: string }; +}): { + [key: string]: Big; +} { + const mapOfBig: { [key: string]: Big } = {}; + + for (const key in value) { + mapOfBig[key] = new Big(value[key]); + } + + return mapOfBig; +} + export function transformToBig({ value }: { value: string }): Big { if (value === null) { return null; diff --git a/libs/common/src/lib/interfaces/portfolio-position.interface.ts b/libs/common/src/lib/interfaces/portfolio-position.interface.ts index 47b3a821d..e277ba468 100644 --- a/libs/common/src/lib/interfaces/portfolio-position.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-position.interface.ts @@ -1,6 +1,7 @@ +import { Market, MarketAdvanced } from '@ghostfolio/common/types'; + import { AssetClass, AssetSubClass, DataSource, Tag } from '@prisma/client'; -import { Market, MarketAdvanced, MarketState } from '../types'; import { Country } from './country.interface'; import { Holding } from './holding.interface'; import { Sector } from './sector.interface'; @@ -28,7 +29,6 @@ export interface PortfolioPosition { marketPrice: number; markets?: { [key in Market]: number }; marketsAdvanced?: { [key in MarketAdvanced]: number }; - marketState: MarketState; name: string; netPerformance: number; netPerformancePercent: number; diff --git a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts index 6b78f273a..6977e7d4c 100644 --- a/libs/common/src/lib/interfaces/symbol-metrics.interface.ts +++ b/libs/common/src/lib/interfaces/symbol-metrics.interface.ts @@ -1,3 +1,5 @@ +import { DateRange } from '@ghostfolio/common/types'; + import { Big } from 'big.js'; export interface SymbolMetrics { @@ -27,11 +29,13 @@ export interface SymbolMetrics { netPerformance: Big; netPerformancePercentage: Big; netPerformancePercentageWithCurrencyEffect: Big; + netPerformancePercentageWithCurrencyEffectMap: { [key: DateRange]: Big }; netPerformanceValues: { [date: string]: Big; }; netPerformanceValuesWithCurrencyEffect: { [date: string]: Big }; netPerformanceWithCurrencyEffect: Big; + netPerformanceWithCurrencyEffectMap: { [key: DateRange]: Big }; timeWeightedInvestment: Big; timeWeightedInvestmentValues: { [date: string]: Big; diff --git a/libs/common/src/lib/models/timeline-position.ts b/libs/common/src/lib/models/timeline-position.ts index 545891464..2345a49fc 100644 --- a/libs/common/src/lib/models/timeline-position.ts +++ b/libs/common/src/lib/models/timeline-position.ts @@ -1,4 +1,8 @@ -import { transformToBig } from '@ghostfolio/common/class-transformer'; +import { + transformToBig, + transformToMapOfBig +} from '@ghostfolio/common/class-transformer'; +import { DateRange } from '@ghostfolio/common/types'; import { DataSource, Tag } from '@prisma/client'; import { Big } from 'big.js'; @@ -69,10 +73,16 @@ export class TimelinePosition { @Type(() => Big) netPerformancePercentageWithCurrencyEffect: Big; + @Transform(transformToMapOfBig, { toClassOnly: true }) + netPerformancePercentageWithCurrencyEffectMap: { [key: DateRange]: Big }; + @Transform(transformToBig, { toClassOnly: true }) @Type(() => Big) netPerformanceWithCurrencyEffect: Big; + @Transform(transformToMapOfBig, { toClassOnly: true }) + netPerformanceWithCurrencyEffectMap: { [key: DateRange]: Big }; + @Transform(transformToBig, { toClassOnly: true }) @Type(() => Big) quantity: Big;