From dcf622aa590dde42c1f6b35f5d121c9f49a20cdc Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 20 Sep 2026 10:46:06 +0200 Subject: [PATCH] Task/rename transactionPoints to holdingBalancesByDate (#7923) Rename transactionPoints to holdingBalancesByDate --- .../calculator/portfolio-calculator.ts | 341 ++++++++---------- ...folio-calculator-msft-buy-and-sell.spec.ts | 20 +- .../portfolio-calculator-stock-split.spec.ts | 48 ++- ...erface.ts => holding-balance.interface.ts} | 3 +- .../holding-balances-at-date.interface.ts | 6 + .../interfaces/transaction-point.interface.ts | 11 - .../app/portfolio/portfolio.service.spec.ts | 4 +- .../src/app/portfolio/portfolio.service.ts | 22 +- 8 files changed, 213 insertions(+), 242 deletions(-) rename apps/api/src/app/portfolio/interfaces/{transaction-point-symbol.interface.ts => holding-balance.interface.ts} (87%) create mode 100644 apps/api/src/app/portfolio/interfaces/holding-balances-at-date.interface.ts delete mode 100644 apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index debfa7d23f..f2ea3a959c 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -1,6 +1,8 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { PortfolioSnapshotComputationError } from '@ghostfolio/api/app/portfolio/errors/portfolio-snapshot-computation.error'; import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface'; +import { HoldingBalance } from '@ghostfolio/api/app/portfolio/interfaces/holding-balance.interface'; +import { HoldingBalancesAtDate } from '@ghostfolio/api/app/portfolio/interfaces/holding-balances-at-date.interface'; import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; import { HoldingValuationItem } from '@ghostfolio/api/app/portfolio/interfaces/holding-valuation-item.interface'; import { HoldingValuation } from '@ghostfolio/api/app/portfolio/interfaces/holding-valuation.interface'; @@ -8,8 +10,6 @@ import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/i import { PortfolioCalculatorActivity } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity.interface'; import { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface'; import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface'; -import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; -import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; @@ -95,13 +95,13 @@ export abstract class PortfolioCalculator { private endDate: Date; private exchangeRateDataService: ExchangeRateDataService; private filters: Filter[]; + private holdingBalancesByDate: HoldingBalancesAtDate[]; private portfolioSnapshotService: PortfolioSnapshotService; private redisCacheService: RedisCacheService; private snapshot: PortfolioSnapshot; private snapshotPromise: Promise; private startDate: Date; private subscriptionType?: SubscriptionType; - private transactionPoints: TransactionPoint[]; private usePortfolioSnapshotCache: boolean; private userId: string; @@ -204,7 +204,7 @@ export abstract class PortfolioCalculator { this.endDate = endOfDay(endDate); this.startDate = startOfDay(startDate); - this.computeTransactionPoints(); + this.computeHoldingBalancesByDate(); this.snapshotPromise = this.usePortfolioSnapshotCache ? this.initialize() @@ -236,13 +236,15 @@ export abstract class PortfolioCalculator { @LogPerformance public async computeSnapshot(): Promise { - const lastTransactionPoint = this.transactionPoints.at(-1); + const latestHoldingBalances = this.holdingBalancesByDate.at(-1); - const transactionPoints = this.transactionPoints?.filter(({ date }) => { - return isBefore(parseDate(date), this.endDate); - }); + const holdingBalancesByDate = this.holdingBalancesByDate?.filter( + ({ date }) => { + return isBefore(parseDate(date), this.endDate); + } + ); - if (!transactionPoints.length) { + if (!holdingBalancesByDate.length) { return { activitiesCount: 0, createdAt: new Date(), @@ -265,8 +267,6 @@ export abstract class PortfolioCalculator { const cashAssetProfileIdentifiers = new Set(); const currencies: { [assetProfileIdentifier: string]: string } = {}; const dataGatheringItems: DataGatheringItem[] = []; - let firstIndex = transactionPoints.length; - let firstTransactionPoint: TransactionPoint = null; let totalCashInBaseCurrency = new Big(0); let totalInterestWithCurrencyEffect = new Big(0); let totalLiabilitiesWithCurrencyEffect = new Big(0); @@ -276,7 +276,7 @@ export abstract class PortfolioCalculator { currency, dataSource, symbol - } of transactionPoints[firstIndex - 1].items) { + } of holdingBalancesByDate.at(-1).holdings) { // Gather data for all assets except CASH if (assetSubClass !== 'CASH') { dataGatheringItems.push({ @@ -288,16 +288,6 @@ export abstract class PortfolioCalculator { currencies[getAssetProfileIdentifier({ dataSource, symbol })] = currency; } - for (let i = 0; i < transactionPoints.length; i++) { - if ( - !isBefore(parseDate(transactionPoints[i].date), this.startDate) && - firstTransactionPoint === null - ) { - firstTransactionPoint = transactionPoints[i]; - firstIndex = i; - } - } - const exchangeRatesByCurrency = await this.exchangeRateDataService.getExchangeRatesByCurrency({ currencies: Array.from(new Set(Object.values(currencies))), @@ -362,10 +352,6 @@ export abstract class PortfolioCalculator { return chartDate; }); - if (firstIndex > 0) { - firstIndex--; - } - const errors: ResponseError['errors'] = []; let hasAnyHoldingPerformanceErrors = false; @@ -390,7 +376,7 @@ export abstract class PortfolioCalculator { }; } = {}; - for (const item of lastTransactionPoint.items) { + for (const item of latestHoldingBalances.holdings) { const assetProfileIdentifier = getAssetProfileIdentifier(item); const marketPriceInBaseCurrency = ( @@ -909,6 +895,10 @@ export abstract class PortfolioCalculator { return this.snapshot.totalFeesWithCurrencyEffect; } + public getHoldingBalancesByDate() { + return this.holdingBalancesByDate; + } + protected abstract getHoldingPerformance({ chartDates, dataSource, @@ -1300,16 +1290,16 @@ export abstract class PortfolioCalculator { } public getInvestments(): { date: string; investment: Big }[] { - if (this.transactionPoints.length === 0) { + if (this.holdingBalancesByDate.length === 0) { return []; } - return this.transactionPoints.map((transactionPoint) => { + return this.holdingBalancesByDate.map(({ date, holdings }) => { return { - date: transactionPoint.date, - investment: transactionPoint.items.reduce( - (investment, transactionPointSymbol) => - investment.plus(transactionPointSymbol.investment), + date, + investment: holdings.reduce( + (investment, { investment: investmentOfHolding }) => + investment.plus(investmentOfHolding), new Big(0) ) }; @@ -1417,9 +1407,9 @@ export abstract class PortfolioCalculator { } } - if (this.transactionPoints?.length > 0) { + if (this.holdingBalancesByDate?.length > 0) { try { - const firstActivityDateString = this.transactionPoints[0].date; + const firstActivityDateString = this.holdingBalancesByDate[0].date; firstActivityDate = firstActivityDateString ? parseDate(firstActivityDateString) : new Date(); @@ -1486,10 +1476,6 @@ export abstract class PortfolioCalculator { }; } - public getTransactionPoints() { - return this.transactionPoints; - } - protected getUnitPriceAtEndDate({ activities, dataSource, @@ -1519,101 +1505,15 @@ export abstract class PortfolioCalculator { return marketPriceAtEndDate; } - private getChartDateMap({ - endDate, - startDate, - step - }: { - endDate: Date; - startDate: Date; - step: number; - }): { [date: string]: true } { - // Create a map of all relevant chart dates: - // 1. Add transaction point dates - const chartDateMap = this.transactionPoints.reduce((result, { date }) => { - result[date] = true; - return result; - }, {}); - - // 2. Add dates between transactions respecting the specified step size - for (const date of eachDayOfInterval( - { end: endDate, start: startDate }, - { step } - )) { - chartDateMap[format(date, DATE_FORMAT)] = true; - } - - if (step > 1) { - // Reduce the step size of last 90 days - for (const date of eachDayOfInterval( - { end: endDate, start: subDays(endDate, 90) }, - { step: 3 } - )) { - chartDateMap[format(date, DATE_FORMAT)] = true; - } - - // Reduce the step size of last 30 days - for (const date of eachDayOfInterval( - { end: endDate, start: subDays(endDate, 30) }, - { step: 1 } - )) { - chartDateMap[format(date, DATE_FORMAT)] = true; - } - } - - // Make sure the end date is present - chartDateMap[format(endDate, DATE_FORMAT)] = true; - - // Make sure some key dates are present - for (const dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) { - const { endDate: dateRangeEnd, startDate: dateRangeStart } = - getIntervalFromDateRange({ dateRange }); - - if ( - !isBefore(dateRangeStart, startDate) && - !isAfter(dateRangeStart, endDate) - ) { - chartDateMap[format(dateRangeStart, DATE_FORMAT)] = true; - } - - if ( - !isBefore(dateRangeEnd, startDate) && - !isAfter(dateRangeEnd, endDate) - ) { - chartDateMap[format(dateRangeEnd, DATE_FORMAT)] = true; - } - } - - // Make sure the first and last date of each calendar year is present - const interval = { start: startDate, end: endDate }; - - for (const date of eachYearOfInterval(interval)) { - const yearStart = startOfYear(date); - const yearEnd = endOfYear(date); - - if (isWithinInterval(yearStart, interval)) { - // Add start of year (YYYY-01-01) - chartDateMap[format(yearStart, DATE_FORMAT)] = true; - } - - if (isWithinInterval(yearEnd, interval)) { - // Add end of year (YYYY-12-31) - chartDateMap[format(yearEnd, DATE_FORMAT)] = true; - } - } - - return chartDateMap; - } - @LogPerformance - private computeTransactionPoints() { - this.transactionPoints = []; - const transactionPointSymbols: { - [assetProfileIdentifier: string]: TransactionPointSymbol; + private computeHoldingBalancesByDate() { + this.holdingBalancesByDate = []; + const holdingBalancesByAssetProfileIdentifier: { + [assetProfileIdentifier: string]: HoldingBalance; } = {}; let lastDate: string = null; - let lastTransactionPoint: TransactionPoint = null; + let latestHoldingBalances: HoldingBalancesAtDate = null; for (const { assetProfile, @@ -1625,7 +1525,7 @@ export abstract class PortfolioCalculator { type, unitPrice } of this.activities) { - let currentTransactionPointItem: TransactionPointSymbol; + let holdingBalance: HoldingBalance; const assetSubClass = assetProfile.assetSubClass; const currency = assetProfile.currency; @@ -1636,33 +1536,33 @@ export abstract class PortfolioCalculator { const assetProfileIdentifier = getAssetProfileIdentifier(assetProfile); - const oldAccumulatedSymbol = - transactionPointSymbols[assetProfileIdentifier]; + const previousHoldingBalance = + holdingBalancesByAssetProfileIdentifier[assetProfileIdentifier]; - if (oldAccumulatedSymbol) { - let investment = oldAccumulatedSymbol.investment; + if (previousHoldingBalance) { + let investment = previousHoldingBalance.investment; let newQuantity = quantity .mul(factor) - .plus(oldAccumulatedSymbol.quantity); + .plus(previousHoldingBalance.quantity); if (type === 'BUY') { - if (oldAccumulatedSymbol.investment.gte(0)) { - investment = oldAccumulatedSymbol.investment.plus( + if (previousHoldingBalance.investment.gte(0)) { + investment = previousHoldingBalance.investment.plus( quantity.mul(unitPrice) ); } else { - investment = oldAccumulatedSymbol.investment.plus( - quantity.mul(oldAccumulatedSymbol.averagePrice) + investment = previousHoldingBalance.investment.plus( + quantity.mul(previousHoldingBalance.averagePrice) ); } } else if (type === 'SELL') { - if (oldAccumulatedSymbol.investment.gt(0)) { - investment = oldAccumulatedSymbol.investment.minus( - quantity.mul(oldAccumulatedSymbol.averagePrice) + if (previousHoldingBalance.investment.gt(0)) { + investment = previousHoldingBalance.investment.minus( + quantity.mul(previousHoldingBalance.averagePrice) ); } else { - investment = oldAccumulatedSymbol.investment.minus( + investment = previousHoldingBalance.investment.minus( quantity.mul(unitPrice) ); } @@ -1674,28 +1574,27 @@ export abstract class PortfolioCalculator { newQuantity = new Big(0); } - currentTransactionPointItem = { + holdingBalance = { assetSubClass, currency, dataSource, investment, skipErrors, symbol, - activitiesCount: oldAccumulatedSymbol.activitiesCount + 1, + activitiesCount: previousHoldingBalance.activitiesCount + 1, averagePrice: newQuantity.eq(0) ? new Big(0) : investment.div(newQuantity).abs(), - dateOfFirstActivity: oldAccumulatedSymbol.dateOfFirstActivity, - dividend: new Big(0), - fee: oldAccumulatedSymbol.fee.plus(fee), + dateOfFirstActivity: previousHoldingBalance.dateOfFirstActivity, + fee: previousHoldingBalance.fee.plus(fee), feeInBaseCurrency: - oldAccumulatedSymbol.feeInBaseCurrency.plus(feeInBaseCurrency), - includeInHoldings: oldAccumulatedSymbol.includeInHoldings, + previousHoldingBalance.feeInBaseCurrency.plus(feeInBaseCurrency), + includeInHoldings: previousHoldingBalance.includeInHoldings, quantity: newQuantity, - tags: oldAccumulatedSymbol.tags.concat(tags) + tags: previousHoldingBalance.tags.concat(tags) }; } else { - currentTransactionPointItem = { + holdingBalance = { assetSubClass, currency, dataSource, @@ -1707,75 +1606,131 @@ export abstract class PortfolioCalculator { activitiesCount: 1, averagePrice: unitPrice, dateOfFirstActivity: date, - dividend: new Big(0), includeInHoldings: INVESTMENT_ACTIVITY_TYPES.includes(type), investment: unitPrice.mul(quantity).mul(factor), quantity: quantity.mul(factor) }; } - currentTransactionPointItem.tags = uniqBy( - currentTransactionPointItem.tags, - 'id' - ); - - transactionPointSymbols[assetProfileIdentifier] = - currentTransactionPointItem; + holdingBalance.tags = uniqBy(holdingBalance.tags, 'id'); - const items = lastTransactionPoint?.items ?? []; + holdingBalancesByAssetProfileIdentifier[assetProfileIdentifier] = + holdingBalance; - const newItems = items.filter((item) => { - return getAssetProfileIdentifier(item) !== assetProfileIdentifier; - }); + const holdings = (latestHoldingBalances?.holdings ?? []).filter( + (holding) => { + return getAssetProfileIdentifier(holding) !== assetProfileIdentifier; + } + ); - newItems.push(currentTransactionPointItem); + holdings.push(holdingBalance); - newItems.sort((a, b) => { + holdings.sort((a, b) => { return ( a.symbol?.localeCompare(b.symbol) || a.dataSource?.localeCompare(b.dataSource) ); }); - let fees = new Big(0); + if (lastDate !== date || latestHoldingBalances === null) { + latestHoldingBalances = { date, holdings }; - if (type === 'FEE') { - fees = fee; + this.holdingBalancesByDate.push(latestHoldingBalances); + } else { + latestHoldingBalances.holdings = holdings; } - let interest = new Big(0); + lastDate = date; + } + } - if (type === 'INTEREST') { - interest = quantity.mul(unitPrice); + private getChartDateMap({ + endDate, + startDate, + step + }: { + endDate: Date; + startDate: Date; + step: number; + }): { [date: string]: true } { + // Create a map of all relevant chart dates: + // 1. Add the dates of the holding balances + const chartDateMap = this.holdingBalancesByDate.reduce( + (result, { date }) => { + result[date] = true; + return result; + }, + {} + ); + + // 2. Add the dates in between, with the specified step size + for (const date of eachDayOfInterval( + { end: endDate, start: startDate }, + { step } + )) { + chartDateMap[format(date, DATE_FORMAT)] = true; + } + + if (step > 1) { + // Reduce the step size of last 90 days + for (const date of eachDayOfInterval( + { end: endDate, start: subDays(endDate, 90) }, + { step: 3 } + )) { + chartDateMap[format(date, DATE_FORMAT)] = true; } - let liabilities = new Big(0); + // Reduce the step size of last 30 days + for (const date of eachDayOfInterval( + { end: endDate, start: subDays(endDate, 30) }, + { step: 1 } + )) { + chartDateMap[format(date, DATE_FORMAT)] = true; + } + } + + // Make sure the end date is present + chartDateMap[format(endDate, DATE_FORMAT)] = true; + + // Make sure some key dates are present + for (const dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) { + const { endDate: dateRangeEnd, startDate: dateRangeStart } = + getIntervalFromDateRange({ dateRange }); - if (type === 'LIABILITY') { - liabilities = quantity.mul(unitPrice); + if ( + !isBefore(dateRangeStart, startDate) && + !isAfter(dateRangeStart, endDate) + ) { + chartDateMap[format(dateRangeStart, DATE_FORMAT)] = true; } - if (lastDate !== date || lastTransactionPoint === null) { - lastTransactionPoint = { - date, - fees, - interest, - liabilities, - items: newItems - }; + if ( + !isBefore(dateRangeEnd, startDate) && + !isAfter(dateRangeEnd, endDate) + ) { + chartDateMap[format(dateRangeEnd, DATE_FORMAT)] = true; + } + } - this.transactionPoints.push(lastTransactionPoint); - } else { - lastTransactionPoint.fees = lastTransactionPoint.fees.plus(fees); - lastTransactionPoint.interest = - lastTransactionPoint.interest.plus(interest); - lastTransactionPoint.items = newItems; - lastTransactionPoint.liabilities = - lastTransactionPoint.liabilities.plus(liabilities); + // Make sure the first and last date of each calendar year is present + const interval = { start: startDate, end: endDate }; + + for (const date of eachYearOfInterval(interval)) { + const yearStart = startOfYear(date); + const yearEnd = endOfYear(date); + + if (isWithinInterval(yearStart, interval)) { + // Add start of year (YYYY-01-01) + chartDateMap[format(yearStart, DATE_FORMAT)] = true; } - lastDate = date; + if (isWithinInterval(yearEnd, interval)) { + // Add end of year (YYYY-12-31) + chartDateMap[format(yearEnd, DATE_FORMAT)] = true; + } } + + return chartDateMap; } @LogPerformance diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts index 228fdd542a..1db23bc6cb 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts @@ -75,7 +75,7 @@ describe('PortfolioCalculator', () => { ); }); - describe('get transaction point', () => { + describe('get holding balances by date', () => { it('with MSFT buy and sell with fractional quantities (multiples of 1/3)', () => { jest.useFakeTimers().setSystemTime(parseDate('2024-04-01').getTime()); @@ -137,15 +137,19 @@ describe('PortfolioCalculator', () => { userId: userDummyData.id }); - const transactionPoints = portfolioCalculator.getTransactionPoints(); - const lastTransactionPoint = - transactionPoints[transactionPoints.length - 1]; - const position = lastTransactionPoint.items.find( - (item) => item.symbol === 'MSFT' + const holdingBalancesByDate = + portfolioCalculator.getHoldingBalancesByDate(); + + const latestHoldingBalances = holdingBalancesByDate.at(-1); + + const { investment, quantity } = latestHoldingBalances.holdings.find( + ({ symbol }) => { + return symbol === 'MSFT'; + } ); - expect(position.investment.toNumber()).toBe(0); - expect(position.quantity.toNumber()).toBe(0); + expect(investment.toNumber()).toBe(0); + expect(quantity.toNumber()).toBe(0); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-stock-split.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-stock-split.spec.ts index e7e2371a4d..f45e453734 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-stock-split.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-stock-split.spec.ts @@ -85,9 +85,11 @@ describe('RoaiPortfolioCalculator stock splits', () => { [createSplit({ denominator: 1, numerator: 2 })] ); - const position = getLastPosition(portfolioCalculatorFactory, [activity]); + const holdingBalance = getLatestHoldingBalance(portfolioCalculatorFactory, [ + activity + ]); - expect(position).toMatchObject({ + expect(holdingBalance).toMatchObject({ averagePrice: new Big(50), investment: new Big(1000), quantity: new Big(20) @@ -100,9 +102,11 @@ describe('RoaiPortfolioCalculator stock splits', () => { [createSplit({ denominator: 10, numerator: 1 })] ); - const position = getLastPosition(portfolioCalculatorFactory, [activity]); + const holdingBalance = getLatestHoldingBalance(portfolioCalculatorFactory, [ + activity + ]); - expect(position).toMatchObject({ + expect(holdingBalance).toMatchObject({ averagePrice: new Big(1000), investment: new Big(1000), quantity: new Big(1) @@ -121,9 +125,12 @@ describe('RoaiPortfolioCalculator stock splits', () => { unitPrice: 60 }); - const position = getLastPosition(portfolioCalculatorFactory, [buy, sell]); + const holdingBalance = getLatestHoldingBalance(portfolioCalculatorFactory, [ + buy, + sell + ]); - expect(position).toMatchObject({ + expect(holdingBalance).toMatchObject({ averagePrice: new Big(50), investment: new Big(750), quantity: new Big(15) @@ -139,11 +146,15 @@ describe('RoaiPortfolioCalculator stock splits', () => { ] ); - const position = getLastPosition(portfolioCalculatorFactory, [activity]); + const holdingBalance = getLatestHoldingBalance(portfolioCalculatorFactory, [ + activity + ]); - expect(position.averagePrice).toEqual(new Big(150)); - expect(position.quantity.toFixed(15)).toBe(new Big(20).div(3).toFixed(15)); - expect(position.investment.toNumber()).toBeCloseTo(1000, 12); + expect(holdingBalance.averagePrice).toEqual(new Big(150)); + expect(holdingBalance.quantity.toFixed(15)).toBe( + new Big(20).div(3).toFixed(15) + ); + expect(holdingBalance.investment.toNumber()).toBeCloseTo(1000, 12); }); it('resets quantity and investment when the adjusted position is closed', () => { @@ -158,18 +169,21 @@ describe('RoaiPortfolioCalculator stock splits', () => { unitPrice: 60 }); - const position = getLastPosition(portfolioCalculatorFactory, [buy, sell]); + const holdingBalance = getLatestHoldingBalance(portfolioCalculatorFactory, [ + buy, + sell + ]); - expect(position.quantity).toEqual(new Big(0)); - expect(position.investment).toEqual(new Big(0)); + expect(holdingBalance.quantity).toEqual(new Big(0)); + expect(holdingBalance.investment).toEqual(new Big(0)); }); it('preserves existing behavior when no splits exist', () => { - const position = getLastPosition(portfolioCalculatorFactory, [ + const holdingBalance = getLatestHoldingBalance(portfolioCalculatorFactory, [ createActivity({ unitPrice: 100 }) ]); - expect(position).toMatchObject({ + expect(holdingBalance).toMatchObject({ averagePrice: new Big(100), investment: new Big(1000), quantity: new Big(10) @@ -208,7 +222,7 @@ describe('RoaiPortfolioCalculator stock splits', () => { }); }); -function getLastPosition( +function getLatestHoldingBalance( portfolioCalculatorFactory: PortfolioCalculatorFactory, activities: Activity[] ) { @@ -219,7 +233,7 @@ function getLastPosition( userId: userDummyData.id }); - return calculator.getTransactionPoints().at(-1).items[0]; + return calculator.getHoldingBalancesByDate().at(-1).holdings[0]; } function createActivity({ diff --git a/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts b/apps/api/src/app/portfolio/interfaces/holding-balance.interface.ts similarity index 87% rename from apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts rename to apps/api/src/app/portfolio/interfaces/holding-balance.interface.ts index 7f3f54ff5e..2eace0ccbd 100644 --- a/apps/api/src/app/portfolio/interfaces/transaction-point-symbol.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/holding-balance.interface.ts @@ -1,14 +1,13 @@ import { AssetSubClass, DataSource, Tag } from '@prisma/client'; import { Big } from 'big.js'; -export interface TransactionPointSymbol { +export interface HoldingBalance { activitiesCount: number; assetSubClass: AssetSubClass; averagePrice: Big; currency: string; dataSource: DataSource; dateOfFirstActivity: string; - dividend: Big; fee: Big; feeInBaseCurrency: Big; includeInHoldings: boolean; diff --git a/apps/api/src/app/portfolio/interfaces/holding-balances-at-date.interface.ts b/apps/api/src/app/portfolio/interfaces/holding-balances-at-date.interface.ts new file mode 100644 index 0000000000..0cfd21f3e3 --- /dev/null +++ b/apps/api/src/app/portfolio/interfaces/holding-balances-at-date.interface.ts @@ -0,0 +1,6 @@ +import { HoldingBalance } from './holding-balance.interface'; + +export interface HoldingBalancesAtDate { + date: string; + holdings: HoldingBalance[]; +} diff --git a/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts b/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts deleted file mode 100644 index 698b202e41..0000000000 --- a/apps/api/src/app/portfolio/interfaces/transaction-point.interface.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Big } from 'big.js'; - -import { TransactionPointSymbol } from './transaction-point-symbol.interface'; - -export interface TransactionPoint { - date: string; - fees: Big; - interest: Big; - items: TransactionPointSymbol[]; - liabilities: Big; -} diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index 3a5ff8f9a5..59addfb44e 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -646,8 +646,8 @@ describe('PortfolioService', () => { jest .spyOn(portfolioCalculatorFactory, 'createCalculator') .mockReturnValue({ - getSnapshot: jest.fn().mockResolvedValue({ positions: [] }), - getTransactionPoints: jest.fn().mockReturnValue([]) + getHoldingBalancesByDate: jest.fn().mockReturnValue([]), + getSnapshot: jest.fn().mockResolvedValue({ positions: [] }) } as unknown as PortfolioCalculator); }); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index a9a1a95faf..1476db4ae0 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -970,7 +970,8 @@ export class PortfolioService { subscriptionType: user.subscription?.type }); - const transactionPoints = portfolioCalculator.getTransactionPoints(); + const holdingBalancesByDate = + portfolioCalculator.getHoldingBalancesByDate(); const { positions } = await portfolioCalculator.getSnapshot(); @@ -1071,8 +1072,11 @@ export class PortfolioService { historicalDataItems )) { while ( - j + 1 < transactionPoints.length && - !isAfter(parseDate(transactionPoints[j + 1].date), parseDate(date)) + j + 1 < holdingBalancesByDate.length && + !isAfter( + parseDate(holdingBalancesByDate[j + 1].date), + parseDate(date) + ) ) { j++; } @@ -1080,15 +1084,15 @@ export class PortfolioService { let currentAveragePrice = 0; let currentQuantity = 0; - const currentSymbol = transactionPoints[j]?.items.find( - (transactionPointSymbol) => { - return transactionPointSymbol.symbol === symbol; + const holdingBalance = holdingBalancesByDate[j]?.holdings.find( + ({ symbol: holdingBalanceSymbol }) => { + return holdingBalanceSymbol === symbol; } ); - if (currentSymbol) { - currentAveragePrice = currentSymbol.averagePrice.toNumber(); - currentQuantity = currentSymbol.quantity.toNumber(); + if (holdingBalance) { + currentAveragePrice = holdingBalance.averagePrice.toNumber(); + currentQuantity = holdingBalance.quantity.toNumber(); } historicalDataArray.push({