diff --git a/CHANGELOG.md b/CHANGELOG.md index 603643dad..f2a952243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the performance of the portfolio snapshot calculation by indexing the activities - Upgraded `angular` from version `21.2.19` to `22.1.2` - Upgraded `ngx-device-detector` from version `11.0.0` to `12.0.0` - Upgraded `ngx-markdown` from version `21.2.0` to `22.0.0` diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index cdab3fdf0..4ec03083d 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -61,7 +61,7 @@ import { startOfYear, subDays } from 'date-fns'; -import { isNumber, sortBy, sum, uniqBy } from 'lodash'; +import { groupBy, isNumber, sortBy, sum, uniqBy } from 'lodash'; export abstract class PortfolioCalculator { protected static readonly ENABLE_LOGGING = false; @@ -72,6 +72,9 @@ export abstract class PortfolioCalculator { protected accountBalanceItems: HistoricalDataItem[]; protected activities: PortfolioOrder[]; + protected activitiesBySymbol: { + [symbol: string]: PortfolioOrder[]; + }; private configurationService: ConfigurationService; private currency: string; @@ -162,6 +165,10 @@ export abstract class PortfolioCalculator { return a.date?.localeCompare(b.date); }); + this.activitiesBySymbol = groupBy(this.activities, ({ assetProfile }) => { + return assetProfile.symbol; + }); + this.portfolioSnapshotService = portfolioSnapshotService; this.redisCacheService = redisCacheService; this.userId = userId; diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts index 69ab70aa8..26deba0b4 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -3,7 +3,7 @@ import { PortfolioCalculatorPosition } from '@ghostfolio/api/app/portfolio/inter 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 { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, SymbolMetrics @@ -21,7 +21,7 @@ import { isBefore, isThisYear } from 'date-fns'; -import { cloneDeep, sortBy } from 'lodash'; +import { sortBy } from 'lodash'; export class RoaiPortfolioCalculator extends PortfolioCalculator { private chartDates: string[]; @@ -192,12 +192,13 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { let valueAtStartDate: Big; let valueAtStartDateWithCurrencyEffect: Big; - // Deep clone as the items are enriched below and the originals are shared - let orders: PortfolioOrderItem[] = cloneDeep( - this.activities.filter((activities) => { - return activities.assetProfile.symbol === symbol; - }) - ); + // Copy the items as they are enriched below. A shallow copy is sufficient + // because only top-level properties are written. + let orders: PortfolioOrderItem[] = ( + this.activitiesBySymbol[symbol] ?? [] + ).map((activity) => { + return { ...activity }; + }); const isCash = orders[0]?.assetProfile?.assetSubClass === 'CASH'; @@ -268,7 +269,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } } - const dateOfFirstTransaction = new Date(orders[0].date); + const dateStringOfFirstActivity = orders[0].date; + const dateOfFirstActivity = parseDate(dateStringOfFirstActivity); const endDateString = format(end, DATE_FORMAT); const startDateString = format(start, DATE_FORMAT); @@ -276,7 +278,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { const unitPriceAtStartDate = marketSymbolMap[startDateString]?.[symbol]; let unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol]; - let latestActivity = orders.at(-1); + const latestActivity = orders.at(-1); if ( dataSource === 'MANUAL' && @@ -293,7 +295,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { if ( !unitPriceAtEndDate || - (!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) + (!unitPriceAtStartDate && isBefore(dateOfFirstActivity, start)) ) { // A missing market price can only affect the quantity which is held. The // dividends, the interest and the liabilities do not hold any quantity @@ -367,7 +369,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { unitPrice: unitPriceAtEndDate }); - let lastUnitPrice: Big; + // Fall back to the unit price at the end date for the chart dates before + // the first known market price of the symbol + let lastUnitPrice = unitPriceAtEndDate; const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {}; @@ -393,23 +397,24 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice; } } else { - orders.push({ - assetProfile, - date: dateString, - fee: new Big(0), - feeInBaseCurrency: new Big(0), - quantity: new Big(0), - type: 'BUY', - unitPrice: marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice, - unitPriceFromMarketData: - marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice - }); - } - - latestActivity = orders.at(-1); + const unitPrice = + marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice; + + if (dateString >= dateStringOfFirstActivity) { + orders.push({ + assetProfile, + unitPrice, + date: dateString, + fee: new Big(0), + feeInBaseCurrency: new Big(0), + quantity: new Big(0), + type: 'BUY', + unitPriceFromMarketData: unitPrice + }); + } - lastUnitPrice = - latestActivity.unitPriceFromMarketData ?? latestActivity.unitPrice; + lastUnitPrice = unitPrice; + } } // Sort orders so that the start and end placeholder order are at the correct