From adae69b0657c5dd37980e62d2b2d0dc766be7473 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 19 Sep 2026 21:20:20 +0200 Subject: [PATCH] Task/rename order to activity in portfolio calculator (#7916) Rename order to activity --- .../calculator/portfolio-calculator.ts | 308 +++++++++--------- .../calculator/roai/portfolio-calculator.ts | 175 +++++----- ...lio-calculator-activity-item.interface.ts} | 4 +- ...ortfolio-calculator-activity.interface.ts} | 5 +- 4 files changed, 250 insertions(+), 242 deletions(-) rename apps/api/src/app/portfolio/interfaces/{portfolio-order-item.interface.ts => portfolio-calculator-activity-item.interface.ts} (56%) rename apps/api/src/app/portfolio/interfaces/{portfolio-order.interface.ts => portfolio-calculator-activity.interface.ts} (75%) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index fb586e8248..b154fc17bc 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -1,9 +1,9 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { PortfolioSnapshotComputationError } from '@ghostfolio/api/app/portfolio/errors/portfolio-snapshot-computation.error'; import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; +import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity-item.interface'; +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 { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface'; -import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.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'; @@ -75,9 +75,9 @@ export abstract class PortfolioCalculator { protected readonly logger = new Logger(PortfolioCalculator.name); protected accountBalanceItems: HistoricalDataItem[]; - protected activities: PortfolioOrder[]; + protected activities: PortfolioCalculatorActivity[]; protected activitiesByAssetProfileIdentifier: { - [assetProfileIdentifier: string]: PortfolioOrder[]; + [assetProfileIdentifier: string]: PortfolioCalculatorActivity[]; }; private configurationService: ConfigurationService; @@ -716,6 +716,139 @@ export abstract class PortfolioCalculator { }; } + protected getActivitiesWithMarketPrices({ + activities, + assetProfile, + chartDates, + endDateString, + marketSymbolMap, + startDateString, + unitPriceAtEndDate, + unitPriceAtStartDate + }: { + activities: PortfolioCalculatorActivityItem[]; + assetProfile: PortfolioCalculatorActivityItem['assetProfile']; + chartDates: string[]; + endDateString: string; + marketSymbolMap: { + [date: string]: { [assetProfileIdentifier: string]: Big }; + }; + startDateString: string; + unitPriceAtEndDate: Big; + unitPriceAtStartDate: Big; + }): PortfolioCalculatorActivityItem[] { + if (activities.length <= 0) { + return []; + } + + const assetProfileIdentifier = getAssetProfileIdentifier(assetProfile); + const dateStringOfFirstActivity = activities[0].date; + + // Copy the items as they are enriched below. A shallow copy is sufficient + // because only top-level properties are written. + const activitiesWithMarketPrices = activities.map((activity) => { + return { ...activity }; + }); + + // Add a synthetic activity at the start and the end date + activitiesWithMarketPrices.push({ + assetProfile, + date: startDateString, + fee: new Big(0), + feeInBaseCurrency: new Big(0), + itemType: 'start', + quantity: new Big(0), + type: 'BUY', + unitPrice: unitPriceAtStartDate + }); + + activitiesWithMarketPrices.push({ + assetProfile, + date: endDateString, + fee: new Big(0), + feeInBaseCurrency: new Big(0), + itemType: 'end', + quantity: new Big(0), + type: 'BUY', + unitPrice: unitPriceAtEndDate + }); + + // Fall back to the unit price of the most recent BUY / SELL activity for + // the chart dates before the first known market price of the symbol + let lastActivityUnitPrice: Big | undefined; + let lastMarketPrice: Big | undefined; + + const activitiesByDate: { + [date: string]: PortfolioCalculatorActivityItem[]; + } = {}; + + for (const activity of activitiesWithMarketPrices) { + activitiesByDate[activity.date] = activitiesByDate[activity.date] ?? []; + activitiesByDate[activity.date].push(activity); + } + + for (const dateString of chartDates) { + if (dateString < startDateString) { + continue; + } else if (dateString > endDateString) { + break; + } + + const activitiesOfDate = activitiesByDate[dateString]; + + if (!lastMarketPrice && activitiesOfDate?.length > 0) { + for (const { itemType, type, unitPrice } of activitiesOfDate) { + if (!itemType && ['BUY', 'SELL'].includes(type)) { + lastActivityUnitPrice = unitPrice; + } + } + } + + const marketPrice = marketSymbolMap[dateString]?.[assetProfileIdentifier]; + + const unitPrice = + marketPrice ?? + lastMarketPrice ?? + lastActivityUnitPrice ?? + unitPriceAtEndDate; + + if (activitiesOfDate?.length > 0) { + for (const activity of activitiesOfDate) { + activity.unitPriceFromMarketData = unitPrice; + } + } else if (dateString >= dateStringOfFirstActivity) { + activitiesWithMarketPrices.push({ + assetProfile, + unitPrice, + date: dateString, + fee: new Big(0), + feeInBaseCurrency: new Big(0), + quantity: new Big(0), + type: 'BUY', + unitPriceFromMarketData: unitPrice + }); + } + + if (marketPrice) { + lastMarketPrice = marketPrice; + } + } + + // Sort the activities so that the start and end placeholder activities + // are at the correct position + return sortBy(activitiesWithMarketPrices, ({ date, itemType }) => { + let sortIndex = new Date(date); + + if (itemType === 'end') { + sortIndex = addMilliseconds(sortIndex, 1); + } else if (itemType === 'start') { + sortIndex = addMilliseconds(sortIndex, -1); + } + + return sortIndex.getTime(); + }); + } + public getDataProviderInfos() { return this.dataProviderInfos; } @@ -837,137 +970,6 @@ export abstract class PortfolioCalculator { return this.snapshot.totalLiabilitiesWithCurrencyEffect; } - protected getOrdersWithMarketPrices({ - assetProfile, - chartDates, - endDateString, - marketSymbolMap, - orders, - startDateString, - unitPriceAtEndDate, - unitPriceAtStartDate - }: { - assetProfile: PortfolioOrderItem['assetProfile']; - chartDates: string[]; - endDateString: string; - marketSymbolMap: { - [date: string]: { [assetProfileIdentifier: string]: Big }; - }; - orders: PortfolioOrderItem[]; - startDateString: string; - unitPriceAtEndDate: Big; - unitPriceAtStartDate: Big; - }): PortfolioOrderItem[] { - if (orders.length <= 0) { - return []; - } - - const assetProfileIdentifier = getAssetProfileIdentifier(assetProfile); - const dateStringOfFirstActivity = orders[0].date; - - // Copy the items as they are enriched below. A shallow copy is sufficient - // because only top-level properties are written. - const ordersWithMarketPrices = orders.map((order) => { - return { ...order }; - }); - - // Add a synthetic order at the start and the end date - ordersWithMarketPrices.push({ - assetProfile, - date: startDateString, - fee: new Big(0), - feeInBaseCurrency: new Big(0), - itemType: 'start', - quantity: new Big(0), - type: 'BUY', - unitPrice: unitPriceAtStartDate - }); - - ordersWithMarketPrices.push({ - assetProfile, - date: endDateString, - fee: new Big(0), - feeInBaseCurrency: new Big(0), - itemType: 'end', - quantity: new Big(0), - type: 'BUY', - unitPrice: unitPriceAtEndDate - }); - - // Fall back to the unit price of the most recent BUY / SELL activity for - // the chart dates before the first known market price of the symbol - let lastActivityUnitPrice: Big | undefined; - let lastMarketPrice: Big | undefined; - - const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {}; - - for (const order of ordersWithMarketPrices) { - ordersByDate[order.date] = ordersByDate[order.date] ?? []; - ordersByDate[order.date].push(order); - } - - for (const dateString of chartDates) { - if (dateString < startDateString) { - continue; - } else if (dateString > endDateString) { - break; - } - - const ordersOfDate = ordersByDate[dateString]; - - if (!lastMarketPrice && ordersOfDate?.length > 0) { - for (const { itemType, type, unitPrice } of ordersOfDate) { - if (!itemType && ['BUY', 'SELL'].includes(type)) { - lastActivityUnitPrice = unitPrice; - } - } - } - - const marketPrice = marketSymbolMap[dateString]?.[assetProfileIdentifier]; - - const unitPrice = - marketPrice ?? - lastMarketPrice ?? - lastActivityUnitPrice ?? - unitPriceAtEndDate; - - if (ordersOfDate?.length > 0) { - for (const order of ordersOfDate) { - order.unitPriceFromMarketData = unitPrice; - } - } else if (dateString >= dateStringOfFirstActivity) { - ordersWithMarketPrices.push({ - assetProfile, - unitPrice, - date: dateString, - fee: new Big(0), - feeInBaseCurrency: new Big(0), - quantity: new Big(0), - type: 'BUY', - unitPriceFromMarketData: unitPrice - }); - } - - if (marketPrice) { - lastMarketPrice = marketPrice; - } - } - - // Sort orders so that the start and end placeholder order are at the correct - // position - return sortBy(ordersWithMarketPrices, ({ date, itemType }) => { - let sortIndex = new Date(date); - - if (itemType === 'end') { - sortIndex = addMilliseconds(sortIndex, 1); - } else if (itemType === 'start') { - sortIndex = addMilliseconds(sortIndex, -1); - } - - return sortIndex.getTime(); - }); - } - public async getPerformance({ end, start }) { await this.snapshotPromise; @@ -1104,38 +1106,38 @@ export abstract class PortfolioCalculator { } protected getTotalsFromActivities({ - exchangeRates, - orders + activities, + exchangeRates }: { + activities: PortfolioCalculatorActivity[]; exchangeRates: { [dateString: string]: number }; - orders: PortfolioOrder[]; }) { let totalDividend = new Big(0); let totalDividendInBaseCurrency = new Big(0); let totalInterestInBaseCurrency = new Big(0); let totalLiabilitiesInBaseCurrency = new Big(0); - for (const order of orders) { - const exchangeRateAtOrderDate = exchangeRates[order.date]; + for (const activity of activities) { + const exchangeRateAtActivityDate = exchangeRates[activity.date]; - if (order.type === 'DIVIDEND') { - const dividend = order.quantity.mul(order.unitPrice); + if (activity.type === 'DIVIDEND') { + const dividend = activity.quantity.mul(activity.unitPrice); totalDividend = totalDividend.plus(dividend); totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus( - dividend.mul(exchangeRateAtOrderDate ?? 1) + dividend.mul(exchangeRateAtActivityDate ?? 1) ); - } else if (order.type === 'INTEREST') { - const interest = order.quantity.mul(order.unitPrice); + } else if (activity.type === 'INTEREST') { + const interest = activity.quantity.mul(activity.unitPrice); totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus( - interest.mul(exchangeRateAtOrderDate ?? 1) + interest.mul(exchangeRateAtActivityDate ?? 1) ); - } else if (order.type === 'LIABILITY') { - const liabilities = order.quantity.mul(order.unitPrice); + } else if (activity.type === 'LIABILITY') { + const liabilities = activity.quantity.mul(activity.unitPrice); totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus( - liabilities.mul(exchangeRateAtOrderDate ?? 1) + liabilities.mul(exchangeRateAtActivityDate ?? 1) ); } } @@ -1153,17 +1155,17 @@ export abstract class PortfolioCalculator { } protected getUnitPriceAtEndDate({ + activities, dataSource, isCash, - marketPriceAtEndDate, - orders + marketPriceAtEndDate }: { + activities: PortfolioCalculatorActivity[]; dataSource: DataSource; isCash: boolean; marketPriceAtEndDate: Big; - orders: PortfolioOrder[]; }): Big { - const latestActivity = orders.at(-1); + const latestActivity = activities.at(-1); if ( dataSource === 'MANUAL' && 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 873bffd4a8..34850f2158 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -1,7 +1,7 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; +import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity-item.interface'; import { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface'; -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 { @@ -183,12 +183,12 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { symbol }); - let orders: PortfolioOrderItem[] = + let activities: PortfolioCalculatorActivityItem[] = this.activitiesByAssetProfileIdentifier[assetProfileIdentifier] ?? []; - const isCash = orders[0]?.assetProfile?.assetSubClass === 'CASH'; + const isCash = activities[0]?.assetProfile?.assetSubClass === 'CASH'; - if (orders.length <= 0) { + if (activities.length <= 0) { return this.getEmptyHoldingPerformance(); } @@ -200,9 +200,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { totalDividendInBaseCurrency, totalInterestInBaseCurrency, totalLiabilitiesInBaseCurrency - } = this.getTotalsFromActivities({ exchangeRates, orders }); + } = this.getTotalsFromActivities({ activities, exchangeRates }); - const dateOfFirstActivity = parseDate(orders[0].date); + const dateOfFirstActivity = parseDate(activities[0].date); const endDateString = format(end, DATE_FORMAT); const startDateString = format(start, DATE_FORMAT); @@ -211,9 +211,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { marketSymbolMap[startDateString]?.[assetProfileIdentifier]; const unitPriceAtEndDate = this.getUnitPriceAtEndDate({ + activities, dataSource, isCash, - orders, marketPriceAtEndDate: marketSymbolMap[endDateString]?.[assetProfileIdentifier] }); @@ -225,7 +225,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { // A missing market price can only affect the quantity which is held. The // dividends, the interest and the liabilities do not hold any quantity // and are therefore not in error. - const hasActivitiesWithQuantity = orders.some(({ type }) => { + const hasActivitiesWithQuantity = activities.some(({ type }) => { return ['BUY', 'SELL'].includes(type); }); @@ -239,11 +239,11 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { }; } - orders = this.getOrdersWithMarketPrices({ + activities = this.getActivitiesWithMarketPrices({ + activities, chartDates, endDateString, marketSymbolMap, - orders, startDateString, unitPriceAtEndDate, unitPriceAtStartDate, @@ -254,11 +254,11 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } }); - const indexOfStartOrder = orders.findIndex(({ itemType }) => { + const indexOfStartActivity = activities.findIndex(({ itemType }) => { return itemType === 'start'; }); - const indexOfEndOrder = orders.findIndex(({ itemType }) => { + const indexOfEndActivity = activities.findIndex(({ itemType }) => { return itemType === 'end'; }); @@ -266,56 +266,59 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { let sumOfTimeWeightedInvestments = new Big(0); let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0); - for (let i = 0; i < orders.length; i += 1) { - const order = orders[i]; + for (let i = 0; i < activities.length; i += 1) { + const activity = activities[i]; if (PortfolioCalculator.ENABLE_LOGGING) { console.log(); console.log(); console.log( i + 1, - order.date, - order.type, - order.itemType ? `(${order.itemType})` : '' + activity.date, + activity.type, + activity.itemType ? `(${activity.itemType})` : '' ); } - const exchangeRateAtOrderDate = exchangeRates[order.date]; + const exchangeRateAtActivityDate = exchangeRates[activity.date]; - if (order.itemType === 'start') { - // Take the unit price of the order as the market price if there are no - // orders of this symbol before the start date - order.unitPrice = - indexOfStartOrder === 0 - ? orders[i + 1]?.unitPrice + if (activity.itemType === 'start') { + // Take the unit price of the activity as the market price if there are no + // activities of this symbol before the start date + activity.unitPrice = + indexOfStartActivity === 0 + ? activities[i + 1]?.unitPrice : unitPriceAtStartDate; } - if (order.fee) { - order.feeInBaseCurrency = order.fee.mul(currentExchangeRate ?? 1); - order.feeInBaseCurrencyWithCurrencyEffect = order.fee.mul( - exchangeRateAtOrderDate ?? 1 + if (activity.fee) { + activity.feeInBaseCurrency = activity.fee.mul(currentExchangeRate ?? 1); + activity.feeInBaseCurrencyWithCurrencyEffect = activity.fee.mul( + exchangeRateAtActivityDate ?? 1 ); } - const unitPrice = ['BUY', 'SELL'].includes(order.type) - ? order.unitPrice - : order.unitPriceFromMarketData; + const unitPrice = ['BUY', 'SELL'].includes(activity.type) + ? activity.unitPrice + : activity.unitPriceFromMarketData; if (unitPrice) { - order.unitPriceInBaseCurrency = unitPrice.mul(currentExchangeRate ?? 1); + activity.unitPriceInBaseCurrency = unitPrice.mul( + currentExchangeRate ?? 1 + ); - order.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul( - exchangeRateAtOrderDate ?? 1 + activity.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul( + exchangeRateAtActivityDate ?? 1 ); } const marketPriceInBaseCurrency = - order.unitPriceFromMarketData?.mul(currentExchangeRate ?? 1) ?? + activity.unitPriceFromMarketData?.mul(currentExchangeRate ?? 1) ?? new Big(0); const marketPriceInBaseCurrencyWithCurrencyEffect = - order.unitPriceFromMarketData?.mul(exchangeRateAtOrderDate ?? 1) ?? - new Big(0); + activity.unitPriceFromMarketData?.mul( + exchangeRateAtActivityDate ?? 1 + ) ?? new Big(0); const valueOfInvestmentBeforeTransaction = totalQuantity.mul( marketPriceInBaseCurrency @@ -324,7 +327,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { const valueOfInvestmentBeforeTransactionWithCurrencyEffect = totalQuantity.mul(marketPriceInBaseCurrencyWithCurrencyEffect); - if (!investmentAtStartDate && i >= indexOfStartOrder) { + if (!investmentAtStartDate && i >= indexOfStartActivity) { investmentAtStartDate = totalInvestment ?? new Big(0); investmentAtStartDateWithCurrencyEffect = @@ -339,17 +342,17 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { let transactionInvestment = new Big(0); let transactionInvestmentWithCurrencyEffect = new Big(0); - if (order.type === 'BUY') { - transactionInvestment = order.quantity - .mul(order.unitPriceInBaseCurrency) - .mul(getFactor(order.type)); + if (activity.type === 'BUY') { + transactionInvestment = activity.quantity + .mul(activity.unitPriceInBaseCurrency) + .mul(getFactor(activity.type)); - transactionInvestmentWithCurrencyEffect = order.quantity - .mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) - .mul(getFactor(order.type)); + transactionInvestmentWithCurrencyEffect = activity.quantity + .mul(activity.unitPriceInBaseCurrencyWithCurrencyEffect) + .mul(getFactor(activity.type)); totalQuantityFromBuyTransactions = - totalQuantityFromBuyTransactions.plus(order.quantity); + totalQuantityFromBuyTransactions.plus(activity.quantity); totalInvestmentFromBuyTransactions = totalInvestmentFromBuyTransactions.plus(transactionInvestment); @@ -358,9 +361,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus( transactionInvestmentWithCurrencyEffect ); - } else if (order.type === 'SELL') { + } else if (activity.type === 'SELL') { if (totalQuantity.gt(0)) { - const remainingQuantity = totalQuantity.minus(order.quantity); + const remainingQuantity = totalQuantity.minus(activity.quantity); transactionInvestment = totalInvestment .mul(remainingQuantity) @@ -376,7 +379,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } if (PortfolioCalculator.ENABLE_LOGGING) { - console.log('order.quantity', order.quantity.toNumber()); + console.log('activity.quantity', activity.quantity.toNumber()); console.log('transactionInvestment', transactionInvestment.toNumber()); console.log( @@ -397,9 +400,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { transactionInvestmentWithCurrencyEffect ); - if (i >= indexOfStartOrder && !initialValue) { + if (i >= indexOfStartActivity && !initialValue) { if ( - i === indexOfStartOrder && + i === indexOfStartActivity && !valueOfInvestmentBeforeTransaction.eq(0) ) { initialValue = valueOfInvestmentBeforeTransaction; @@ -408,14 +411,14 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } } - fees = fees.plus(order.feeInBaseCurrency ?? 0); + fees = fees.plus(activity.feeInBaseCurrency ?? 0); feesWithCurrencyEffect = feesWithCurrencyEffect.plus( - order.feeInBaseCurrencyWithCurrencyEffect ?? 0 + activity.feeInBaseCurrencyWithCurrencyEffect ?? 0 ); totalQuantity = totalQuantity.plus( - order.quantity.mul(getFactor(order.type)) + activity.quantity.mul(getFactor(activity.type)) ); const valueOfInvestment = totalQuantity.mul(marketPriceInBaseCurrency); @@ -425,17 +428,17 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { ); const grossPerformanceFromSell = - order.type === 'SELL' - ? order.unitPriceInBaseCurrency + activity.type === 'SELL' + ? activity.unitPriceInBaseCurrency .minus(lastAveragePrice) - .mul(order.quantity) + .mul(activity.quantity) : new Big(0); const grossPerformanceFromSellWithCurrencyEffect = - order.type === 'SELL' - ? order.unitPriceInBaseCurrencyWithCurrencyEffect + activity.type === 'SELL' + ? activity.unitPriceInBaseCurrencyWithCurrencyEffect .minus(lastAveragePriceWithCurrencyEffect) - .mul(order.quantity) + .mul(activity.quantity) : new Big(0); grossPerformanceFromSells = grossPerformanceFromSells.plus( @@ -493,7 +496,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { grossPerformanceWithCurrencyEffect = newGrossPerformanceWithCurrencyEffect; - if (order.itemType === 'start') { + if (activity.itemType === 'start') { feesAtStartDate = fees; feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect; grossPerformanceAtStartDate = grossPerformance; @@ -502,36 +505,36 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { grossPerformanceWithCurrencyEffect; } - if (i > indexOfStartOrder) { + if (i > indexOfStartActivity) { // Only consider periods with an investment for the calculation of // the time weighted investment if ( valueOfInvestmentBeforeTransaction.gt(0) && - ['BUY', 'SELL'].includes(order.type) + ['BUY', 'SELL'].includes(activity.type) ) { - // Calculate the number of days since the previous order - const orderDate = new Date(order.date); - const previousOrderDate = new Date(orders[i - 1].date); + // Calculate the number of days since the previous activity + const activityDate = new Date(activity.date); + const previousActivityDate = new Date(activities[i - 1].date); - let daysSinceLastOrder = differenceInDays( - orderDate, - previousOrderDate + let daysSinceLastActivity = differenceInDays( + activityDate, + previousActivityDate ); - if (daysSinceLastOrder <= 0) { + if (daysSinceLastActivity <= 0) { // The time between two activities on the same day is unknown // -> Set it to the smallest floating point number greater than 0 - daysSinceLastOrder = Number.EPSILON; + daysSinceLastActivity = Number.EPSILON; } // Sum up the total investment days since the start date to calculate // the time weighted investment - totalInvestmentDays += daysSinceLastOrder; + totalInvestmentDays += daysSinceLastActivity; sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add( valueAtStartDate .minus(investmentAtStartDate) .plus(totalInvestmentBeforeTransaction) - .mul(daysSinceLastOrder) + .mul(daysSinceLastActivity) ); sumOfTimeWeightedInvestmentsWithCurrencyEffect = @@ -539,45 +542,45 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { valueAtStartDateWithCurrencyEffect .minus(investmentAtStartDateWithCurrencyEffect) .plus(totalInvestmentBeforeTransactionWithCurrencyEffect) - .mul(daysSinceLastOrder) + .mul(daysSinceLastActivity) ); } - currentValues[order.date] = valueOfInvestment; + currentValues[activity.date] = valueOfInvestment; - currentValuesWithCurrencyEffect[order.date] = + currentValuesWithCurrencyEffect[activity.date] = valueOfInvestmentWithCurrencyEffect; - netPerformanceValues[order.date] = grossPerformance + netPerformanceValues[activity.date] = grossPerformance .minus(grossPerformanceAtStartDate) .minus(fees.minus(feesAtStartDate)); - netPerformanceValuesWithCurrencyEffect[order.date] = + netPerformanceValuesWithCurrencyEffect[activity.date] = grossPerformanceWithCurrencyEffect .minus(grossPerformanceAtStartDateWithCurrencyEffect) .minus( feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect) ); - investmentValuesAccumulated[order.date] = totalInvestment; + investmentValuesAccumulated[activity.date] = totalInvestment; - investmentValuesAccumulatedWithCurrencyEffect[order.date] = + investmentValuesAccumulatedWithCurrencyEffect[activity.date] = totalInvestmentWithCurrencyEffect; - investmentValuesWithCurrencyEffect[order.date] = ( - investmentValuesWithCurrencyEffect[order.date] ?? new Big(0) + investmentValuesWithCurrencyEffect[activity.date] = ( + investmentValuesWithCurrencyEffect[activity.date] ?? new Big(0) ).add(transactionInvestmentWithCurrencyEffect); // If duration is effectively zero (first day), use the actual investment as the base. // Otherwise, use the calculated time-weighted average. - timeWeightedInvestmentValues[order.date] = + timeWeightedInvestmentValues[activity.date] = totalInvestmentDays > Number.EPSILON ? sumOfTimeWeightedInvestments.div(totalInvestmentDays) : totalInvestment.gt(0) ? totalInvestment : new Big(0); - timeWeightedInvestmentValuesWithCurrencyEffect[order.date] = + timeWeightedInvestmentValuesWithCurrencyEffect[activity.date] = totalInvestmentDays > Number.EPSILON ? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( totalInvestmentDays @@ -608,7 +611,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { ); } - if (i === indexOfEndOrder) { + if (i === indexOfEndActivity) { break; } } @@ -768,7 +771,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { console.log( ` ${symbol} - Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed( + Unit price: ${activities[indexOfStartActivity].unitPrice.toFixed( 2 )} -> ${unitPriceAtEndDate.toFixed(2)} Total investment: ${totalInvestment.toFixed(2)} diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-order-item.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-calculator-activity-item.interface.ts similarity index 56% rename from apps/api/src/app/portfolio/interfaces/portfolio-order-item.interface.ts rename to apps/api/src/app/portfolio/interfaces/portfolio-calculator-activity-item.interface.ts index 42759b5213..7ed82ea0fb 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-order-item.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-calculator-activity-item.interface.ts @@ -1,8 +1,8 @@ import { Big } from 'big.js'; -import { PortfolioOrder } from './portfolio-order.interface'; +import { PortfolioCalculatorActivity } from './portfolio-calculator-activity.interface'; -export interface PortfolioOrderItem extends PortfolioOrder { +export interface PortfolioCalculatorActivityItem extends PortfolioCalculatorActivity { feeInBaseCurrencyWithCurrencyEffect?: Big; itemType?: 'end' | 'start'; unitPriceFromMarketData?: Big; diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-calculator-activity.interface.ts similarity index 75% rename from apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts rename to apps/api/src/app/portfolio/interfaces/portfolio-calculator-activity.interface.ts index becab8c046..811e774175 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-calculator-activity.interface.ts @@ -1,6 +1,9 @@ import { Activity } from '@ghostfolio/common/interfaces'; -export interface PortfolioOrder extends Pick { +export interface PortfolioCalculatorActivity extends Pick< + Activity, + 'tags' | 'type' +> { assetProfile: Pick< Activity['assetProfile'], 'assetSubClass' | 'currency' | 'dataSource' | 'name' | 'symbol' | 'userId'