diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index b154fc17bc..124346812f 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 { 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'; 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'; @@ -76,6 +78,7 @@ export abstract class PortfolioCalculator { protected accountBalanceItems: HistoricalDataItem[]; protected activities: PortfolioCalculatorActivity[]; + protected activitiesByAssetProfileIdentifier: { [assetProfileIdentifier: string]: PortfolioCalculatorActivity[]; }; @@ -918,6 +921,372 @@ export abstract class PortfolioCalculator { start: Date; } & AssetProfileIdentifier): HoldingPerformance; + protected getHoldingValuation({ + activities, + exchangeRates, + unitPriceAtStartDate + }: { + activities: PortfolioCalculatorActivityItem[]; + exchangeRates: { [dateString: string]: number }; + unitPriceAtStartDate: Big | undefined; + }): HoldingValuation { + const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; + const currentValues: { [date: string]: Big } = {}; + const currentValuesWithCurrencyEffect: { [date: string]: Big } = {}; + let fees = new Big(0); + let feesAtStartDate = new Big(0); + let feesAtStartDateWithCurrencyEffect = new Big(0); + let feesWithCurrencyEffect = new Big(0); + let grossPerformance = new Big(0); + let grossPerformanceWithCurrencyEffect = new Big(0); + let grossPerformanceAtStartDate = new Big(0); + let grossPerformanceAtStartDateWithCurrencyEffect = new Big(0); + let grossPerformanceFromSells = new Big(0); + let grossPerformanceFromSellsWithCurrencyEffect = new Big(0); + let initialValue: Big; + const investmentValuesAccumulated: { [date: string]: Big } = {}; + + const investmentValuesAccumulatedWithCurrencyEffect: { + [date: string]: Big; + } = {}; + + const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {}; + const items: HoldingValuationItem[] = []; + let lastAveragePrice = new Big(0); + let lastAveragePriceWithCurrencyEffect = new Big(0); + const netPerformanceValues: { [date: string]: Big } = {}; + const netPerformanceValuesWithCurrencyEffect: { [date: string]: Big } = {}; + let totalInvestment = new Big(0); + let totalInvestmentFromBuyTransactions = new Big(0); + let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0); + let totalInvestmentWithCurrencyEffect = new Big(0); + let totalQuantity = new Big(0); + let totalQuantityFromBuyTransactions = new Big(0); + + const indexOfStartActivity = activities.findIndex(({ itemType }) => { + return itemType === 'start'; + }); + + const indexOfEndActivity = activities.findIndex(({ itemType }) => { + return itemType === 'end'; + }); + + 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, + activity.date, + activity.type, + activity.itemType ? `(${activity.itemType})` : '' + ); + } + + const exchangeRateAtActivityDate = exchangeRates[activity.date]; + + 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 (activity.fee) { + activity.feeInBaseCurrency = activity.fee.mul(currentExchangeRate ?? 1); + activity.feeInBaseCurrencyWithCurrencyEffect = activity.fee.mul( + exchangeRateAtActivityDate ?? 1 + ); + } + + const unitPrice = ['BUY', 'SELL'].includes(activity.type) + ? activity.unitPrice + : activity.unitPriceFromMarketData; + + if (unitPrice) { + activity.unitPriceInBaseCurrency = unitPrice.mul( + currentExchangeRate ?? 1 + ); + + activity.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul( + exchangeRateAtActivityDate ?? 1 + ); + } + + const marketPriceInBaseCurrency = + activity.unitPriceFromMarketData?.mul(currentExchangeRate ?? 1) ?? + new Big(0); + const marketPriceInBaseCurrencyWithCurrencyEffect = + activity.unitPriceFromMarketData?.mul( + exchangeRateAtActivityDate ?? 1 + ) ?? new Big(0); + + const valueOfInvestmentBeforeTransaction = totalQuantity.mul( + marketPriceInBaseCurrency + ); + + const valueOfInvestmentBeforeTransactionWithCurrencyEffect = + totalQuantity.mul(marketPriceInBaseCurrencyWithCurrencyEffect); + + let transactionInvestment = new Big(0); + let transactionInvestmentWithCurrencyEffect = new Big(0); + + if (activity.type === 'BUY') { + transactionInvestment = activity.quantity + .mul(activity.unitPriceInBaseCurrency) + .mul(getFactor(activity.type)); + + transactionInvestmentWithCurrencyEffect = activity.quantity + .mul(activity.unitPriceInBaseCurrencyWithCurrencyEffect) + .mul(getFactor(activity.type)); + + totalQuantityFromBuyTransactions = + totalQuantityFromBuyTransactions.plus(activity.quantity); + + totalInvestmentFromBuyTransactions = + totalInvestmentFromBuyTransactions.plus(transactionInvestment); + + totalInvestmentFromBuyTransactionsWithCurrencyEffect = + totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus( + transactionInvestmentWithCurrencyEffect + ); + } else if (activity.type === 'SELL') { + if (totalQuantity.gt(0)) { + const remainingQuantity = totalQuantity.minus(activity.quantity); + + transactionInvestment = totalInvestment + .mul(remainingQuantity) + .div(totalQuantity) + .minus(totalInvestment); + + transactionInvestmentWithCurrencyEffect = + totalInvestmentWithCurrencyEffect + .mul(remainingQuantity) + .div(totalQuantity) + .minus(totalInvestmentWithCurrencyEffect); + } + } + + if (PortfolioCalculator.ENABLE_LOGGING) { + console.log('activity.quantity', activity.quantity.toNumber()); + console.log('transactionInvestment', transactionInvestment.toNumber()); + + console.log( + 'transactionInvestmentWithCurrencyEffect', + transactionInvestmentWithCurrencyEffect.toNumber() + ); + } + + const totalInvestmentBeforeTransaction = totalInvestment; + + const totalInvestmentBeforeTransactionWithCurrencyEffect = + totalInvestmentWithCurrencyEffect; + + totalInvestment = totalInvestment.plus(transactionInvestment); + + totalInvestmentWithCurrencyEffect = + totalInvestmentWithCurrencyEffect.plus( + transactionInvestmentWithCurrencyEffect + ); + + if (i >= indexOfStartActivity && !initialValue) { + if ( + i === indexOfStartActivity && + !valueOfInvestmentBeforeTransaction.eq(0) + ) { + initialValue = valueOfInvestmentBeforeTransaction; + } else if (transactionInvestment.gt(0)) { + initialValue = transactionInvestment; + } + } + + fees = fees.plus(activity.feeInBaseCurrency ?? 0); + + feesWithCurrencyEffect = feesWithCurrencyEffect.plus( + activity.feeInBaseCurrencyWithCurrencyEffect ?? 0 + ); + + totalQuantity = totalQuantity.plus( + activity.quantity.mul(getFactor(activity.type)) + ); + + const valueOfInvestment = totalQuantity.mul(marketPriceInBaseCurrency); + + const valueOfInvestmentWithCurrencyEffect = totalQuantity.mul( + marketPriceInBaseCurrencyWithCurrencyEffect + ); + + const grossPerformanceFromSell = + activity.type === 'SELL' + ? activity.unitPriceInBaseCurrency + .minus(lastAveragePrice) + .mul(activity.quantity) + : new Big(0); + + const grossPerformanceFromSellWithCurrencyEffect = + activity.type === 'SELL' + ? activity.unitPriceInBaseCurrencyWithCurrencyEffect + .minus(lastAveragePriceWithCurrencyEffect) + .mul(activity.quantity) + : new Big(0); + + grossPerformanceFromSells = grossPerformanceFromSells.plus( + grossPerformanceFromSell + ); + + grossPerformanceFromSellsWithCurrencyEffect = + grossPerformanceFromSellsWithCurrencyEffect.plus( + grossPerformanceFromSellWithCurrencyEffect + ); + + lastAveragePrice = totalQuantityFromBuyTransactions.eq(0) + ? new Big(0) + : totalInvestmentFromBuyTransactions.div( + totalQuantityFromBuyTransactions + ); + + lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq( + 0 + ) + ? new Big(0) + : totalInvestmentFromBuyTransactionsWithCurrencyEffect.div( + totalQuantityFromBuyTransactions + ); + + if (totalQuantity.eq(0)) { + // Reset tracking variables when position is fully closed + totalInvestmentFromBuyTransactions = new Big(0); + totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0); + totalQuantityFromBuyTransactions = new Big(0); + } + + if (PortfolioCalculator.ENABLE_LOGGING) { + console.log( + 'grossPerformanceFromSells', + grossPerformanceFromSells.toNumber() + ); + console.log( + 'grossPerformanceFromSellWithCurrencyEffect', + grossPerformanceFromSellWithCurrencyEffect.toNumber() + ); + } + + const newGrossPerformance = valueOfInvestment + .minus(totalInvestment) + .plus(grossPerformanceFromSells); + + const newGrossPerformanceWithCurrencyEffect = + valueOfInvestmentWithCurrencyEffect + .minus(totalInvestmentWithCurrencyEffect) + .plus(grossPerformanceFromSellsWithCurrencyEffect); + + grossPerformance = newGrossPerformance; + + grossPerformanceWithCurrencyEffect = + newGrossPerformanceWithCurrencyEffect; + + if (activity.itemType === 'start') { + feesAtStartDate = fees; + feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect; + grossPerformanceAtStartDate = grossPerformance; + + grossPerformanceAtStartDateWithCurrencyEffect = + grossPerformanceWithCurrencyEffect; + } + + if (i > indexOfStartActivity) { + currentValues[activity.date] = valueOfInvestment; + + currentValuesWithCurrencyEffect[activity.date] = + valueOfInvestmentWithCurrencyEffect; + + netPerformanceValues[activity.date] = grossPerformance + .minus(grossPerformanceAtStartDate) + .minus(fees.minus(feesAtStartDate)); + + netPerformanceValuesWithCurrencyEffect[activity.date] = + grossPerformanceWithCurrencyEffect + .minus(grossPerformanceAtStartDateWithCurrencyEffect) + .minus( + feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect) + ); + + investmentValuesAccumulated[activity.date] = totalInvestment; + + investmentValuesAccumulatedWithCurrencyEffect[activity.date] = + totalInvestmentWithCurrencyEffect; + + investmentValuesWithCurrencyEffect[activity.date] = ( + investmentValuesWithCurrencyEffect[activity.date] ?? new Big(0) + ).add(transactionInvestmentWithCurrencyEffect); + } + + if (PortfolioCalculator.ENABLE_LOGGING) { + console.log('totalInvestment', totalInvestment.toNumber()); + + console.log( + 'totalInvestmentWithCurrencyEffect', + totalInvestmentWithCurrencyEffect.toNumber() + ); + + console.log( + 'totalGrossPerformance', + grossPerformance.minus(grossPerformanceAtStartDate).toNumber() + ); + + console.log( + 'totalGrossPerformanceWithCurrencyEffect', + grossPerformanceWithCurrencyEffect + .minus(grossPerformanceAtStartDateWithCurrencyEffect) + .toNumber() + ); + } + + items.push({ + fees, + feesWithCurrencyEffect, + grossPerformance, + grossPerformanceWithCurrencyEffect, + transactionInvestment, + transactionInvestmentWithCurrencyEffect, + date: activity.date, + investment: totalInvestment, + investmentBeforeTransaction: totalInvestmentBeforeTransaction, + investmentBeforeTransactionWithCurrencyEffect: + totalInvestmentBeforeTransactionWithCurrencyEffect, + investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, + itemType: activity.itemType, + quantity: totalQuantity, + type: activity.type, + value: valueOfInvestment, + valueBeforeTransaction: valueOfInvestmentBeforeTransaction, + valueBeforeTransactionWithCurrencyEffect: + valueOfInvestmentBeforeTransactionWithCurrencyEffect, + valueWithCurrencyEffect: valueOfInvestmentWithCurrencyEffect + }); + + if (i === indexOfEndActivity) { + break; + } + } + + return { + currentValues, + currentValuesWithCurrencyEffect, + initialValue, + investmentValuesAccumulated, + investmentValuesAccumulatedWithCurrencyEffect, + investmentValuesWithCurrencyEffect, + items, + netPerformanceValues, + netPerformanceValuesWithCurrencyEffect + }; + } + public async getInterestInBaseCurrency() { await this.snapshotPromise; 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 34850f2158..6be9a577e2 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -2,7 +2,6 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/po 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 { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { DATE_FORMAT, @@ -138,43 +137,14 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { }; start: Date; } & AssetProfileIdentifier): HoldingPerformance { - const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; - const currentValues: { [date: string]: Big } = {}; - const currentValuesWithCurrencyEffect: { [date: string]: Big } = {}; - let fees = new Big(0); - let feesAtStartDate = new Big(0); - let feesAtStartDateWithCurrencyEffect = new Big(0); - let feesWithCurrencyEffect = new Big(0); - let grossPerformance = new Big(0); - let grossPerformanceWithCurrencyEffect = new Big(0); - let grossPerformanceAtStartDate = new Big(0); - let grossPerformanceAtStartDateWithCurrencyEffect = new Big(0); - let grossPerformanceFromSells = new Big(0); - let grossPerformanceFromSellsWithCurrencyEffect = new Big(0); - let initialValue: Big; let investmentAtStartDate: Big; let investmentAtStartDateWithCurrencyEffect: Big; - const investmentValuesAccumulated: { [date: string]: Big } = {}; - const investmentValuesAccumulatedWithCurrencyEffect: { - [date: string]: Big; - } = {}; - const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {}; - let lastAveragePrice = new Big(0); - let lastAveragePriceWithCurrencyEffect = new Big(0); - const netPerformanceValues: { [date: string]: Big } = {}; - const netPerformanceValuesWithCurrencyEffect: { [date: string]: Big } = {}; const timeWeightedInvestmentValues: { [date: string]: Big } = {}; const timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big; } = {}; - let totalInvestment = new Big(0); - let totalInvestmentFromBuyTransactions = new Big(0); - let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0); - let totalInvestmentWithCurrencyEffect = new Big(0); - let totalQuantity = new Big(0); - let totalQuantityFromBuyTransactions = new Big(0); let valueAtStartDate: Big; let valueAtStartDateWithCurrencyEffect: Big; @@ -254,11 +224,27 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } }); - const indexOfStartActivity = activities.findIndex(({ itemType }) => { + const { + currentValues, + currentValuesWithCurrencyEffect, + initialValue, + investmentValuesAccumulated, + investmentValuesAccumulatedWithCurrencyEffect, + investmentValuesWithCurrencyEffect, + items, + netPerformanceValues, + netPerformanceValuesWithCurrencyEffect + } = this.getHoldingValuation({ + activities, + exchangeRates, + unitPriceAtStartDate + }); + + const indexOfStartActivity = items.findIndex(({ itemType }) => { return itemType === 'start'; }); - const indexOfEndActivity = activities.findIndex(({ itemType }) => { + const indexOfEndActivity = items.findIndex(({ itemType }) => { return itemType === 'end'; }); @@ -266,255 +252,31 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { let sumOfTimeWeightedInvestments = new Big(0); let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0); - 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, - activity.date, - activity.type, - activity.itemType ? `(${activity.itemType})` : '' - ); - } - - const exchangeRateAtActivityDate = exchangeRates[activity.date]; - - 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 (activity.fee) { - activity.feeInBaseCurrency = activity.fee.mul(currentExchangeRate ?? 1); - activity.feeInBaseCurrencyWithCurrencyEffect = activity.fee.mul( - exchangeRateAtActivityDate ?? 1 - ); - } - - const unitPrice = ['BUY', 'SELL'].includes(activity.type) - ? activity.unitPrice - : activity.unitPriceFromMarketData; - - if (unitPrice) { - activity.unitPriceInBaseCurrency = unitPrice.mul( - currentExchangeRate ?? 1 - ); - - activity.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul( - exchangeRateAtActivityDate ?? 1 - ); - } - - const marketPriceInBaseCurrency = - activity.unitPriceFromMarketData?.mul(currentExchangeRate ?? 1) ?? - new Big(0); - const marketPriceInBaseCurrencyWithCurrencyEffect = - activity.unitPriceFromMarketData?.mul( - exchangeRateAtActivityDate ?? 1 - ) ?? new Big(0); - - const valueOfInvestmentBeforeTransaction = totalQuantity.mul( - marketPriceInBaseCurrency - ); - - const valueOfInvestmentBeforeTransactionWithCurrencyEffect = - totalQuantity.mul(marketPriceInBaseCurrencyWithCurrencyEffect); + for (let i = 0; i < items.length; i += 1) { + const item = items[i]; if (!investmentAtStartDate && i >= indexOfStartActivity) { - investmentAtStartDate = totalInvestment ?? new Big(0); + investmentAtStartDate = item.investmentBeforeTransaction; investmentAtStartDateWithCurrencyEffect = - totalInvestmentWithCurrencyEffect ?? new Big(0); + item.investmentBeforeTransactionWithCurrencyEffect; - valueAtStartDate = valueOfInvestmentBeforeTransaction; + valueAtStartDate = item.valueBeforeTransaction; valueAtStartDateWithCurrencyEffect = - valueOfInvestmentBeforeTransactionWithCurrencyEffect; - } - - let transactionInvestment = new Big(0); - let transactionInvestmentWithCurrencyEffect = new Big(0); - - if (activity.type === 'BUY') { - transactionInvestment = activity.quantity - .mul(activity.unitPriceInBaseCurrency) - .mul(getFactor(activity.type)); - - transactionInvestmentWithCurrencyEffect = activity.quantity - .mul(activity.unitPriceInBaseCurrencyWithCurrencyEffect) - .mul(getFactor(activity.type)); - - totalQuantityFromBuyTransactions = - totalQuantityFromBuyTransactions.plus(activity.quantity); - - totalInvestmentFromBuyTransactions = - totalInvestmentFromBuyTransactions.plus(transactionInvestment); - - totalInvestmentFromBuyTransactionsWithCurrencyEffect = - totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus( - transactionInvestmentWithCurrencyEffect - ); - } else if (activity.type === 'SELL') { - if (totalQuantity.gt(0)) { - const remainingQuantity = totalQuantity.minus(activity.quantity); - - transactionInvestment = totalInvestment - .mul(remainingQuantity) - .div(totalQuantity) - .minus(totalInvestment); - - transactionInvestmentWithCurrencyEffect = - totalInvestmentWithCurrencyEffect - .mul(remainingQuantity) - .div(totalQuantity) - .minus(totalInvestmentWithCurrencyEffect); - } - } - - if (PortfolioCalculator.ENABLE_LOGGING) { - console.log('activity.quantity', activity.quantity.toNumber()); - console.log('transactionInvestment', transactionInvestment.toNumber()); - - console.log( - 'transactionInvestmentWithCurrencyEffect', - transactionInvestmentWithCurrencyEffect.toNumber() - ); - } - - const totalInvestmentBeforeTransaction = totalInvestment; - - const totalInvestmentBeforeTransactionWithCurrencyEffect = - totalInvestmentWithCurrencyEffect; - - totalInvestment = totalInvestment.plus(transactionInvestment); - - totalInvestmentWithCurrencyEffect = - totalInvestmentWithCurrencyEffect.plus( - transactionInvestmentWithCurrencyEffect - ); - - if (i >= indexOfStartActivity && !initialValue) { - if ( - i === indexOfStartActivity && - !valueOfInvestmentBeforeTransaction.eq(0) - ) { - initialValue = valueOfInvestmentBeforeTransaction; - } else if (transactionInvestment.gt(0)) { - initialValue = transactionInvestment; - } - } - - fees = fees.plus(activity.feeInBaseCurrency ?? 0); - - feesWithCurrencyEffect = feesWithCurrencyEffect.plus( - activity.feeInBaseCurrencyWithCurrencyEffect ?? 0 - ); - - totalQuantity = totalQuantity.plus( - activity.quantity.mul(getFactor(activity.type)) - ); - - const valueOfInvestment = totalQuantity.mul(marketPriceInBaseCurrency); - - const valueOfInvestmentWithCurrencyEffect = totalQuantity.mul( - marketPriceInBaseCurrencyWithCurrencyEffect - ); - - const grossPerformanceFromSell = - activity.type === 'SELL' - ? activity.unitPriceInBaseCurrency - .minus(lastAveragePrice) - .mul(activity.quantity) - : new Big(0); - - const grossPerformanceFromSellWithCurrencyEffect = - activity.type === 'SELL' - ? activity.unitPriceInBaseCurrencyWithCurrencyEffect - .minus(lastAveragePriceWithCurrencyEffect) - .mul(activity.quantity) - : new Big(0); - - grossPerformanceFromSells = grossPerformanceFromSells.plus( - grossPerformanceFromSell - ); - - grossPerformanceFromSellsWithCurrencyEffect = - grossPerformanceFromSellsWithCurrencyEffect.plus( - grossPerformanceFromSellWithCurrencyEffect - ); - - lastAveragePrice = totalQuantityFromBuyTransactions.eq(0) - ? new Big(0) - : totalInvestmentFromBuyTransactions.div( - totalQuantityFromBuyTransactions - ); - - lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq( - 0 - ) - ? new Big(0) - : totalInvestmentFromBuyTransactionsWithCurrencyEffect.div( - totalQuantityFromBuyTransactions - ); - - if (totalQuantity.eq(0)) { - // Reset tracking variables when position is fully closed - totalInvestmentFromBuyTransactions = new Big(0); - totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0); - totalQuantityFromBuyTransactions = new Big(0); - } - - if (PortfolioCalculator.ENABLE_LOGGING) { - console.log( - 'grossPerformanceFromSells', - grossPerformanceFromSells.toNumber() - ); - console.log( - 'grossPerformanceFromSellWithCurrencyEffect', - grossPerformanceFromSellWithCurrencyEffect.toNumber() - ); - } - - const newGrossPerformance = valueOfInvestment - .minus(totalInvestment) - .plus(grossPerformanceFromSells); - - const newGrossPerformanceWithCurrencyEffect = - valueOfInvestmentWithCurrencyEffect - .minus(totalInvestmentWithCurrencyEffect) - .plus(grossPerformanceFromSellsWithCurrencyEffect); - - grossPerformance = newGrossPerformance; - - grossPerformanceWithCurrencyEffect = - newGrossPerformanceWithCurrencyEffect; - - if (activity.itemType === 'start') { - feesAtStartDate = fees; - feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect; - grossPerformanceAtStartDate = grossPerformance; - - grossPerformanceAtStartDateWithCurrencyEffect = - grossPerformanceWithCurrencyEffect; + item.valueBeforeTransactionWithCurrencyEffect; } if (i > indexOfStartActivity) { // Only consider periods with an investment for the calculation of // the time weighted investment if ( - valueOfInvestmentBeforeTransaction.gt(0) && - ['BUY', 'SELL'].includes(activity.type) + item.valueBeforeTransaction.gt(0) && + ['BUY', 'SELL'].includes(item.type) ) { // Calculate the number of days since the previous activity - const activityDate = new Date(activity.date); - const previousActivityDate = new Date(activities[i - 1].date); + const activityDate = new Date(item.date); + const previousActivityDate = new Date(items[i - 1].date); let daysSinceLastActivity = differenceInDays( activityDate, @@ -533,7 +295,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add( valueAtStartDate .minus(investmentAtStartDate) - .plus(totalInvestmentBeforeTransaction) + .plus(item.investmentBeforeTransaction) .mul(daysSinceLastActivity) ); @@ -541,81 +303,53 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { sumOfTimeWeightedInvestmentsWithCurrencyEffect.add( valueAtStartDateWithCurrencyEffect .minus(investmentAtStartDateWithCurrencyEffect) - .plus(totalInvestmentBeforeTransactionWithCurrencyEffect) + .plus(item.investmentBeforeTransactionWithCurrencyEffect) .mul(daysSinceLastActivity) ); } - currentValues[activity.date] = valueOfInvestment; - - currentValuesWithCurrencyEffect[activity.date] = - valueOfInvestmentWithCurrencyEffect; - - netPerformanceValues[activity.date] = grossPerformance - .minus(grossPerformanceAtStartDate) - .minus(fees.minus(feesAtStartDate)); - - netPerformanceValuesWithCurrencyEffect[activity.date] = - grossPerformanceWithCurrencyEffect - .minus(grossPerformanceAtStartDateWithCurrencyEffect) - .minus( - feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect) - ); - - investmentValuesAccumulated[activity.date] = totalInvestment; - - investmentValuesAccumulatedWithCurrencyEffect[activity.date] = - totalInvestmentWithCurrencyEffect; - - 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[activity.date] = + timeWeightedInvestmentValues[item.date] = totalInvestmentDays > Number.EPSILON ? sumOfTimeWeightedInvestments.div(totalInvestmentDays) - : totalInvestment.gt(0) - ? totalInvestment + : item.investment.gt(0) + ? item.investment : new Big(0); - timeWeightedInvestmentValuesWithCurrencyEffect[activity.date] = + timeWeightedInvestmentValuesWithCurrencyEffect[item.date] = totalInvestmentDays > Number.EPSILON ? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( totalInvestmentDays ) - : totalInvestmentWithCurrencyEffect.gt(0) - ? totalInvestmentWithCurrencyEffect + : item.investmentWithCurrencyEffect.gt(0) + ? item.investmentWithCurrencyEffect : new Big(0); } - if (PortfolioCalculator.ENABLE_LOGGING) { - console.log('totalInvestment', totalInvestment.toNumber()); - - console.log( - 'totalInvestmentWithCurrencyEffect', - totalInvestmentWithCurrencyEffect.toNumber() - ); - - console.log( - 'totalGrossPerformance', - grossPerformance.minus(grossPerformanceAtStartDate).toNumber() - ); - - console.log( - 'totalGrossPerformanceWithCurrencyEffect', - grossPerformanceWithCurrencyEffect - .minus(grossPerformanceAtStartDateWithCurrencyEffect) - .toNumber() - ); - } - if (i === indexOfEndActivity) { break; } } + const { + fees: feesAtStartDate, + feesWithCurrencyEffect: feesAtStartDateWithCurrencyEffect, + grossPerformance: grossPerformanceAtStartDate, + grossPerformanceWithCurrencyEffect: + grossPerformanceAtStartDateWithCurrencyEffect + } = items[indexOfStartActivity]; + + const { + fees, + feesWithCurrencyEffect, + grossPerformance, + grossPerformanceWithCurrencyEffect, + investment: totalInvestment, + investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, + quantity: totalQuantity + } = items[indexOfEndActivity]; + const totalGrossPerformance = grossPerformance.minus( grossPerformanceAtStartDate ); diff --git a/apps/api/src/app/portfolio/interfaces/holding-valuation-item.interface.ts b/apps/api/src/app/portfolio/interfaces/holding-valuation-item.interface.ts new file mode 100644 index 0000000000..9a5ff4fcd2 --- /dev/null +++ b/apps/api/src/app/portfolio/interfaces/holding-valuation-item.interface.ts @@ -0,0 +1,24 @@ +import { Big } from 'big.js'; + +import { PortfolioCalculatorActivityItem } from './portfolio-calculator-activity-item.interface'; + +export interface HoldingValuationItem extends Pick< + PortfolioCalculatorActivityItem, + 'date' | 'itemType' | 'type' +> { + fees: Big; + feesWithCurrencyEffect: Big; + grossPerformance: Big; + grossPerformanceWithCurrencyEffect: Big; + investment: Big; + investmentBeforeTransaction: Big; + investmentBeforeTransactionWithCurrencyEffect: Big; + investmentWithCurrencyEffect: Big; + quantity: Big; + transactionInvestment: Big; + transactionInvestmentWithCurrencyEffect: Big; + value: Big; + valueBeforeTransaction: Big; + valueBeforeTransactionWithCurrencyEffect: Big; + valueWithCurrencyEffect: Big; +} diff --git a/apps/api/src/app/portfolio/interfaces/holding-valuation.interface.ts b/apps/api/src/app/portfolio/interfaces/holding-valuation.interface.ts new file mode 100644 index 0000000000..9e13fb70b4 --- /dev/null +++ b/apps/api/src/app/portfolio/interfaces/holding-valuation.interface.ts @@ -0,0 +1,15 @@ +import { Big } from 'big.js'; + +import { HoldingValuationItem } from './holding-valuation-item.interface'; + +export interface HoldingValuation { + currentValues: { [date: string]: Big }; + currentValuesWithCurrencyEffect: { [date: string]: Big }; + initialValue?: Big; + investmentValuesAccumulated: { [date: string]: Big }; + investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big }; + investmentValuesWithCurrencyEffect: { [date: string]: Big }; + items: HoldingValuationItem[]; + netPerformanceValues: { [date: string]: Big }; + netPerformanceValuesWithCurrencyEffect: { [date: string]: Big }; +}