From 371ce7ee93226e6e07060aa34d0a259654e41e6c Mon Sep 17 00:00:00 2001 From: Daniel Devaud Date: Sun, 17 Dec 2023 15:44:43 +0100 Subject: [PATCH] Added Timeweighted performance + huge refactoring --- .../src/app/portfolio/portfolio-calculator.ts | 1476 ++++++++++++----- .../src/app/portfolio/portfolio.controller.ts | 6 +- .../src/app/portfolio/portfolio.service.ts | 14 +- 3 files changed, 1115 insertions(+), 381 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 16b41136c..263992bfa 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -85,52 +85,13 @@ export class PortfolioCalculator { const factor = this.getFactor(order.type); const unitPrice = new Big(order.unitPrice); - if (oldAccumulatedSymbol) { - const newQuantity = order.quantity - .mul(factor) - .plus(oldAccumulatedSymbol.quantity); - - let investment = new Big(0); - - if (newQuantity.gt(0)) { - if (order.type === 'BUY' || order.type === 'STAKE') { - investment = oldAccumulatedSymbol.investment.plus( - order.quantity.mul(unitPrice) - ); - } else if (order.type === 'SELL') { - const averagePrice = oldAccumulatedSymbol.investment.div( - oldAccumulatedSymbol.quantity - ); - investment = oldAccumulatedSymbol.investment.minus( - order.quantity.mul(averagePrice) - ); - } - } - - currentTransactionPointItem = { - investment, - currency: order.currency, - dataSource: order.dataSource, - fee: order.fee.plus(oldAccumulatedSymbol.fee), - firstBuyDate: oldAccumulatedSymbol.firstBuyDate, - quantity: newQuantity, - symbol: order.symbol, - tags: order.tags, - transactionCount: oldAccumulatedSymbol.transactionCount + 1 - }; - } else { - currentTransactionPointItem = { - currency: order.currency, - dataSource: order.dataSource, - fee: order.fee, - firstBuyDate: order.date, - investment: unitPrice.mul(order.quantity).mul(factor), - quantity: order.quantity.mul(factor), - symbol: order.symbol, - tags: order.tags, - transactionCount: 1 - }; - } + currentTransactionPointItem = this.getCurrentTransactionPointItem( + oldAccumulatedSymbol, + order, + factor, + unitPrice, + currentTransactionPointItem + ); symbols[order.symbol] = currentTransactionPointItem; @@ -153,6 +114,79 @@ export class PortfolioCalculator { } } + private getCurrentTransactionPointItem( + oldAccumulatedSymbol: TransactionPointSymbol, + order: PortfolioOrder, + factor: number, + unitPrice: Big, + currentTransactionPointItem: TransactionPointSymbol + ) { + if (oldAccumulatedSymbol) { + currentTransactionPointItem = this.handleSubsequentTransactions( + order, + factor, + oldAccumulatedSymbol, + unitPrice, + currentTransactionPointItem + ); + } else { + currentTransactionPointItem = { + currency: order.currency, + dataSource: order.dataSource, + fee: order.fee, + firstBuyDate: order.date, + investment: unitPrice.mul(order.quantity).mul(factor), + quantity: order.quantity.mul(factor), + symbol: order.symbol, + tags: order.tags, + transactionCount: 1 + }; + } + return currentTransactionPointItem; + } + + private handleSubsequentTransactions( + order: PortfolioOrder, + factor: number, + oldAccumulatedSymbol: TransactionPointSymbol, + unitPrice: Big, + currentTransactionPointItem: TransactionPointSymbol + ) { + const newQuantity = order.quantity + .mul(factor) + .plus(oldAccumulatedSymbol.quantity); + + let investment = new Big(0); + + if (newQuantity.gt(0)) { + if (order.type === 'BUY' || order.type === 'STAKE') { + investment = oldAccumulatedSymbol.investment.plus( + order.quantity.mul(unitPrice) + ); + } else if (order.type === 'SELL') { + const averagePrice = oldAccumulatedSymbol.investment.div( + oldAccumulatedSymbol.quantity + ); + investment = oldAccumulatedSymbol.investment.minus( + order.quantity.mul(averagePrice) + ); + } + } + + currentTransactionPointItem = { + investment, + currency: order.currency, + dataSource: order.dataSource, + fee: order.fee.plus(oldAccumulatedSymbol.fee), + firstBuyDate: oldAccumulatedSymbol.firstBuyDate, + quantity: newQuantity, + symbol: order.symbol, + tags: order.tags, + transactionCount: oldAccumulatedSymbol.transactionCount + 1 + }; + return currentTransactionPointItem; + } + public getAnnualizedPerformancePercent({ daysInMarket, netPerformancePercent @@ -178,7 +212,12 @@ export class PortfolioCalculator { this.transactionPoints = transactionPoints; } - public async getChartData(start: Date, end = new Date(Date.now()), step = 1) { + public async getChartData( + start: Date, + end = new Date(Date.now()), + step = 1, + calculateTimeWeightedPerformance = false + ) { const symbols: { [symbol: string]: boolean } = {}; const transactionPointsBeforeEndDate = @@ -191,35 +230,33 @@ export class PortfolioCalculator { const dataGatheringItems: IDataGatheringItem[] = []; const firstIndex = transactionPointsBeforeEndDate.length; - let day = start; - - while (isBefore(day, end)) { - dates.push(resetHours(day)); - day = addDays(day, step); - } - - if (!isSameDay(last(dates), end)) { - dates.push(resetHours(end)); - } - - for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) { - dataGatheringItems.push({ - dataSource: item.dataSource, - symbol: item.symbol - }); - currencies[item.symbol] = item.currency; - symbols[item.symbol] = true; - } + this.pushDataGatheringsSymbols( + transactionPointsBeforeEndDate, + firstIndex, + dataGatheringItems, + currencies, + symbols + ); + this.getRelevantStartAndEndDates(start, end, dates, step); + const dataGartheringDates = [ + ...dates, + ...this.orders.map((o) => { + let dateParsed = Date.parse(o.date); + if (isBefore(dateParsed, end) && isAfter(dateParsed, start)) { + let date = new Date(dateParsed); + if (dates.indexOf(date) === -1) { + return date; + } + } + }) + ]; const { dataProviderInfos, values: marketSymbols } = - await this.currentRateService.getValues({ + await this.getInformationFromCurrentRateService( currencies, dataGatheringItems, - dateQuery: { - in: dates - }, - userCurrency: this.currency - }); + dataGartheringDates + ); this.dataProviderInfos = dataProviderInfos; @@ -227,42 +264,143 @@ export class PortfolioCalculator { [date: string]: { [symbol: string]: Big }; } = {}; - for (const marketSymbol of marketSymbols) { - const dateString = format(marketSymbol.date, DATE_FORMAT); - if (!marketSymbolMap[dateString]) { - marketSymbolMap[dateString] = {}; + this.populateMarketSymbolMap(marketSymbols, marketSymbolMap); + + const valuesBySymbol: { + [symbol: string]: { + currentValues: { [date: string]: Big }; + investmentValues: { [date: string]: Big }; + maxInvestmentValues: { [date: string]: Big }; + netPerformanceValues: { [date: string]: Big }; + netPerformanceValuesPercentage: { [date: string]: Big }; + }; + } = {}; + + this.populateSymbolMetrics( + symbols, + end, + marketSymbolMap, + start, + step, + valuesBySymbol + ); + + return dates.map((date: Date, index: number, dates: Date[]) => { + let previousDate: Date = index > 0 ? dates[index - 1] : null; + return this.calculatePerformance( + date, + previousDate, + valuesBySymbol, + calculateTimeWeightedPerformance + ); + }); + } + + private calculatePerformance( + date: Date, + previousDate: Date, + valuesBySymbol: { + [symbol: string]: { + currentValues: { [date: string]: Big }; + investmentValues: { [date: string]: Big }; + maxInvestmentValues: { [date: string]: Big }; + netPerformanceValues: { [date: string]: Big }; + netPerformanceValuesPercentage: { [date: string]: Big }; + }; + }, + calculateTimeWeightedPerformance: boolean + ) { + const dateString = format(date, DATE_FORMAT); + const previousDateString = previousDate + ? format(previousDate, DATE_FORMAT) + : null; + let totalCurrentValue = new Big(0); + let totalInvestmentValue = new Big(0); + let maxTotalInvestmentValue = new Big(0); + let totalNetPerformanceValue = new Big(0); + let previousTotalInvestmentValue = new Big(0); + let timeWeightedPerformance = new Big(0); + + if (calculateTimeWeightedPerformance && previousDateString) { + for (const symbol of Object.keys(valuesBySymbol)) { + const symbolValues = valuesBySymbol[symbol]; + previousTotalInvestmentValue = previousTotalInvestmentValue.plus( + symbolValues.currentValues?.[previousDateString] ?? new Big(0) + ); } - if (marketSymbol.marketPriceInBaseCurrency) { - marketSymbolMap[dateString][marketSymbol.symbol] = new Big( - marketSymbol.marketPriceInBaseCurrency + } + + for (const symbol of Object.keys(valuesBySymbol)) { + const symbolValues = valuesBySymbol[symbol]; + const symbolCurrentValues = + symbolValues.currentValues?.[dateString] ?? new Big(0); + + totalCurrentValue = totalCurrentValue.plus(symbolCurrentValues); + totalInvestmentValue = totalInvestmentValue.plus( + symbolValues.investmentValues?.[dateString] ?? new Big(0) + ); + maxTotalInvestmentValue = maxTotalInvestmentValue.plus( + symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0) + ); + totalNetPerformanceValue = totalNetPerformanceValue.plus( + symbolValues.netPerformanceValues?.[dateString] ?? new Big(0) + ); + + if ( + calculateTimeWeightedPerformance && + previousTotalInvestmentValue.toNumber() && + symbolValues.netPerformanceValuesPercentage + ) { + const previousValue = + symbolValues.currentValues?.[previousDateString] ?? new Big(0); + const netPerformance = + symbolValues.netPerformanceValuesPercentage?.[dateString] ?? + new Big(0); + timeWeightedPerformance = timeWeightedPerformance.plus( + previousValue.div(previousTotalInvestmentValue).mul(netPerformance) ); } } + const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0) + ? 0 + : totalNetPerformanceValue + .div(maxTotalInvestmentValue) + .mul(100) + .toNumber(); - const valuesByDate: { - [date: string]: { - maxTotalInvestmentValue: Big; - totalCurrentValue: Big; - totalInvestmentValue: Big; - totalNetPerformanceValue: Big; - }; - } = {}; + return { + date: dateString, + netPerformanceInPercentage, + netPerformance: totalNetPerformanceValue.toNumber(), + totalInvestment: totalInvestmentValue.toNumber(), + value: totalCurrentValue.toNumber(), + timeWeightedPerformance: timeWeightedPerformance.toNumber() + }; + } - const valuesBySymbol: { + private populateSymbolMetrics( + symbols: { [symbol: string]: boolean }, + end: Date, + marketSymbolMap: { [date: string]: { [symbol: string]: Big } }, + start: Date, + step: number, + valuesBySymbol: { [symbol: string]: { currentValues: { [date: string]: Big }; investmentValues: { [date: string]: Big }; maxInvestmentValues: { [date: string]: Big }; netPerformanceValues: { [date: string]: Big }; + netPerformanceValuesPercentage: { [date: string]: Big }; }; - } = {}; - + } + ) { for (const symbol of Object.keys(symbols)) { const { currentValues, investmentValues, maxInvestmentValues, - netPerformanceValues + netPerformanceValues, + netPerformanceValuesPercentage } = this.getSymbolMetrics({ end, marketSymbolMap, @@ -276,50 +414,82 @@ export class PortfolioCalculator { currentValues, investmentValues, maxInvestmentValues, - netPerformanceValues + netPerformanceValues, + netPerformanceValuesPercentage }; } + } - return dates.map((date) => { - const dateString = format(date, DATE_FORMAT); - let totalCurrentValue = new Big(0); - let totalInvestmentValue = new Big(0); - let maxTotalInvestmentValue = new Big(0); - let totalNetPerformanceValue = new Big(0); - - for (const symbol of Object.keys(valuesBySymbol)) { - const symbolValues = valuesBySymbol[symbol]; - - totalCurrentValue = totalCurrentValue.plus( - symbolValues.currentValues?.[dateString] ?? new Big(0) - ); - totalInvestmentValue = totalInvestmentValue.plus( - symbolValues.investmentValues?.[dateString] ?? new Big(0) - ); - maxTotalInvestmentValue = maxTotalInvestmentValue.plus( - symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0) - ); - totalNetPerformanceValue = totalNetPerformanceValue.plus( - symbolValues.netPerformanceValues?.[dateString] ?? new Big(0) + private populateMarketSymbolMap( + marketSymbols: GetValueObject[], + marketSymbolMap: { [date: string]: { [symbol: string]: Big } } + ) { + for (const marketSymbol of marketSymbols) { + const dateString = format(marketSymbol.date, DATE_FORMAT); + if (!marketSymbolMap[dateString]) { + marketSymbolMap[dateString] = {}; + } + if (marketSymbol.marketPriceInBaseCurrency) { + marketSymbolMap[dateString][marketSymbol.symbol] = new Big( + marketSymbol.marketPriceInBaseCurrency ); } - const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0) - ? 0 - : totalNetPerformanceValue - .div(maxTotalInvestmentValue) - .mul(100) - .toNumber(); + } + } - return { - date: dateString, - netPerformanceInPercentage, - netPerformance: totalNetPerformanceValue.toNumber(), - totalInvestment: totalInvestmentValue.toNumber(), - value: totalCurrentValue.toNumber() - }; + private async getInformationFromCurrentRateService( + currencies: { [symbol: string]: string }, + dataGatheringItems: IDataGatheringItem[], + dates: Date[] + ): Promise<{ + dataProviderInfos: DataProviderInfo[]; + values: GetValueObject[]; + }> { + return await this.currentRateService.getValues({ + currencies, + dataGatheringItems, + dateQuery: { + in: dates + }, + userCurrency: this.currency }); } + private pushDataGatheringsSymbols( + transactionPointsBeforeEndDate: TransactionPoint[], + firstIndex: number, + dataGatheringItems: IDataGatheringItem[], + currencies: { [symbol: string]: string }, + symbols: { [symbol: string]: boolean } + ) { + for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) { + dataGatheringItems.push({ + dataSource: item.dataSource, + symbol: item.symbol + }); + currencies[item.symbol] = item.currency; + symbols[item.symbol] = true; + } + } + + private getRelevantStartAndEndDates( + start: Date, + end: Date, + dates: Date[], + step: number + ) { + let day = start; + + while (isBefore(day, end)) { + dates.push(resetHours(day)); + day = addDays(day, step); + } + + if (!isSameDay(last(dates), end)) { + dates.push(resetHours(end)); + } + } + public async getCurrentPositions( start: Date, end = new Date(Date.now()) @@ -975,7 +1145,8 @@ export class PortfolioCalculator { maxInvestmentValues: {}, netPerformance: new Big(0), netPerformancePercentage: new Big(0), - netPerformanceValues: {} + netPerformanceValues: {}, + netPerformanceValuesPercentage: {} }; } @@ -1016,102 +1187,39 @@ export class PortfolioCalculator { let lastAveragePrice = new Big(0); let maxTotalInvestment = new Big(0); const netPerformanceValues: { [date: string]: Big } = {}; + const netPerformanceValuesPercentage: { [date: string]: Big } = {}; let totalInvestment = new Big(0); let totalInvestmentWithGrossPerformanceFromSell = new Big(0); let totalUnits = new Big(0); let valueAtStartDate: Big; // Add a synthetic order at the start and the end date - orders.push({ - symbol, - currency: null, - date: format(start, DATE_FORMAT), - dataSource: null, - fee: new Big(0), - itemType: 'start', - name: '', - quantity: new Big(0), - type: TypeOfOrder.BUY, - unitPrice: unitPriceAtStartDate - }); - - orders.push({ + this.addSyntheticStartAndEndOrders( + orders, symbol, - currency: null, - date: format(end, DATE_FORMAT), - dataSource: null, - fee: new Big(0), - itemType: 'end', - name: '', - quantity: new Big(0), - type: TypeOfOrder.BUY, - unitPrice: unitPriceAtEndDate - }); + start, + unitPriceAtStartDate, + end, + unitPriceAtEndDate + ); let day = start; let lastUnitPrice: Big; - if (isChartMode) { - const datesWithOrders = {}; - - for (const order of orders) { - datesWithOrders[order.date] = true; - } - - while (isBefore(day, end)) { - const hasDate = datesWithOrders[format(day, DATE_FORMAT)]; - - if (!hasDate) { - orders.push({ - symbol, - currency: null, - date: format(day, DATE_FORMAT), - dataSource: null, - fee: new Big(0), - name: '', - quantity: new Big(0), - type: TypeOfOrder.BUY, - unitPrice: - marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? - lastUnitPrice - }); - } else { - let orderIndex = orders.findIndex( - (o) => o.date === format(day, DATE_FORMAT) && o.type === 'STAKE' - ); - if (orderIndex >= 0) { - let order = orders[orderIndex]; - orders.splice(orderIndex, 1); - orders.push({ - ...order, - unitPrice: - marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? - lastUnitPrice - }); - } - } - - lastUnitPrice = last(orders).unitPrice; - - day = addDays(day, step); - } - } + ({ day, lastUnitPrice } = this.handleChartMode( + isChartMode, + orders, + day, + end, + symbol, + marketSymbolMap, + lastUnitPrice, + step + )); // Sort orders so that the start and end placeholder order are at the right // position - orders = sortBy(orders, (order) => { - let sortIndex = new Date(order.date); - - if (order.itemType === 'start') { - sortIndex = addMilliseconds(sortIndex, -1); - } - - if (order.itemType === 'end') { - sortIndex = addMilliseconds(sortIndex, 1); - } - - return sortIndex.getTime(); - }); + orders = this.sortOrdersByTime(orders); const indexOfStartOrder = orders.findIndex((order) => { return order.itemType === 'start'; @@ -1121,121 +1229,290 @@ export class PortfolioCalculator { return order.itemType === 'end'; }); - for (let i = 0; i < orders.length; i += 1) { - const order = orders[i]; - - if (PortfolioCalculator.ENABLE_LOGGING) { - console.log(); - console.log(); - console.log(i + 1, order.type, order.itemType); - } - - 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 - : unitPriceAtStartDate; - } - - // Calculate the average start price as soon as any units are held - if ( - averagePriceAtStartDate.eq(0) && - i >= indexOfStartOrder && - totalUnits.gt(0) - ) { - averagePriceAtStartDate = totalInvestment.div(totalUnits); - } - - const valueOfInvestmentBeforeTransaction = totalUnits.mul( - order.unitPrice - ); + return this.calculatePerformanceOfSymbol( + orders, + indexOfStartOrder, + unitPriceAtStartDate, + averagePriceAtStartDate, + totalUnits, + totalInvestment, + investmentAtStartDate, + valueAtStartDate, + maxTotalInvestment, + indexOfEndOrder, + averagePriceAtEndDate, + initialValue, + marketSymbolMap, + fees, + lastAveragePrice, + grossPerformanceFromSells, + totalInvestmentWithGrossPerformanceFromSell, + grossPerformance, + feesAtStartDate, + grossPerformanceAtStartDate, + isChartMode, + currentValues, + netPerformanceValues, + netPerformanceValuesPercentage, + investmentValues, + maxInvestmentValues, + unitPriceAtEndDate, + symbol + ); + } - if (!investmentAtStartDate && i >= indexOfStartOrder) { - investmentAtStartDate = totalInvestment ?? new Big(0); - valueAtStartDate = valueOfInvestmentBeforeTransaction; - } + private calculatePerformanceOfSymbol( + orders: PortfolioOrderItem[], + indexOfStartOrder: number, + unitPriceAtStartDate: Big, + averagePriceAtStartDate: Big, + totalUnits: Big, + totalInvestment: Big, + investmentAtStartDate: Big, + valueAtStartDate: Big, + maxTotalInvestment: Big, + indexOfEndOrder: number, + averagePriceAtEndDate: Big, + initialValue: Big, + marketSymbolMap: { [date: string]: { [symbol: string]: Big } }, + fees: Big, + lastAveragePrice: Big, + grossPerformanceFromSells: Big, + totalInvestmentWithGrossPerformanceFromSell: Big, + grossPerformance: Big, + feesAtStartDate: Big, + grossPerformanceAtStartDate: Big, + isChartMode: boolean, + currentValues: { [date: string]: Big }, + netPerformanceValues: { [date: string]: Big }, + netPerformanceValuesPercentage: { [date: string]: Big }, + investmentValues: { [date: string]: Big }, + maxInvestmentValues: { [date: string]: Big }, + unitPriceAtEndDate: Big, + symbol: string + ) { + ({ + lastAveragePrice, + grossPerformance, + feesAtStartDate, + grossPerformanceAtStartDate, + averagePriceAtStartDate, + totalUnits, + totalInvestment, + investmentAtStartDate, + valueAtStartDate, + maxTotalInvestment, + averagePriceAtEndDate, + initialValue, + fees, + netPerformanceValuesPercentage + } = this.handleOrders( + orders, + indexOfStartOrder, + unitPriceAtStartDate, + averagePriceAtStartDate, + totalUnits, + totalInvestment, + investmentAtStartDate, + valueAtStartDate, + maxTotalInvestment, + averagePriceAtEndDate, + initialValue, + fees, + indexOfEndOrder, + marketSymbolMap, + grossPerformanceFromSells, + totalInvestmentWithGrossPerformanceFromSell, + lastAveragePrice, + grossPerformance, + feesAtStartDate, + grossPerformanceAtStartDate, + isChartMode, + currentValues, + netPerformanceValues, + netPerformanceValuesPercentage, + investmentValues, + maxInvestmentValues + )); - const transactionInvestment = - order.type === 'BUY' || order.type === 'STAKE' - ? order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type)) - : totalUnits.gt(0) - ? totalInvestment - .div(totalUnits) - .mul(order.quantity) - .mul(this.getFactor(order.type)) - : new Big(0); + const totalGrossPerformance = grossPerformance.minus( + grossPerformanceAtStartDate + ); - if (PortfolioCalculator.ENABLE_LOGGING) { - console.log('totalInvestment', totalInvestment.toNumber()); - console.log('order.quantity', order.quantity.toNumber()); - console.log('transactionInvestment', transactionInvestment.toNumber()); - } + const totalNetPerformance = grossPerformance + .minus(grossPerformanceAtStartDate) + .minus(fees.minus(feesAtStartDate)); - totalInvestment = totalInvestment.plus(transactionInvestment); + const maxInvestmentBetweenStartAndEndDate = valueAtStartDate.plus( + maxTotalInvestment.minus(investmentAtStartDate) + ); - if (i >= indexOfStartOrder && totalInvestment.gt(maxTotalInvestment)) { - maxTotalInvestment = totalInvestment; - } + const grossPerformancePercentage = this.calculateGrossPerformancePercentage( + averagePriceAtStartDate, + averagePriceAtEndDate, + orders, + indexOfStartOrder, + maxInvestmentBetweenStartAndEndDate, + totalGrossPerformance, + unitPriceAtEndDate + ); - if (i === indexOfEndOrder && totalUnits.gt(0)) { - averagePriceAtEndDate = totalInvestment.div(totalUnits); - } + const feesPerUnit = totalUnits.gt(0) + ? fees.minus(feesAtStartDate).div(totalUnits) + : new Big(0); - if (i >= indexOfStartOrder && !initialValue) { - if ( - i === indexOfStartOrder && - !valueOfInvestmentBeforeTransaction.eq(0) - ) { - initialValue = valueOfInvestmentBeforeTransaction; - } else if (transactionInvestment.gt(0)) { - initialValue = transactionInvestment; - } else if (order.type === 'STAKE') { - // For Parachain Rewards or Stock SpinOffs, first transactionInvestment might be 0 if the symbol has been acquired for free - initialValue = order.quantity.mul( - marketSymbolMap[order.date]?.[order.symbol] ?? new Big(0) - ); - } - } + const netPerformancePercentage = this.calculateNetPerformancePercentage( + averagePriceAtStartDate, + averagePriceAtEndDate, + orders, + indexOfStartOrder, + maxInvestmentBetweenStartAndEndDate, + totalNetPerformance, + unitPriceAtEndDate, + feesPerUnit + ); - fees = fees.plus(order.fee); + this.handleLogging( + symbol, + orders, + indexOfStartOrder, + unitPriceAtEndDate, + averagePriceAtStartDate, + averagePriceAtEndDate, + totalInvestment, + maxTotalInvestment, + totalGrossPerformance, + grossPerformancePercentage, + feesPerUnit, + totalNetPerformance, + netPerformancePercentage + ); + return { + currentValues, + grossPerformancePercentage, + initialValue, + investmentValues, + maxInvestmentValues, + netPerformancePercentage, + netPerformanceValues, + grossPerformance: totalGrossPerformance, + hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), + netPerformance: totalNetPerformance, + averagePriceAtStartDate, + totalUnits, + totalInvestment, + investmentAtStartDate, + valueAtStartDate, + maxTotalInvestment, + averagePriceAtEndDate, + fees, + lastAveragePrice, + grossPerformanceFromSells, + totalInvestmentWithGrossPerformanceFromSell, + feesAtStartDate, + grossPerformanceAtStartDate, + netPerformanceValuesPercentage + }; + } - totalUnits = totalUnits.plus( - order.quantity.mul(this.getFactor(order.type)) + private handleOrders( + orders: PortfolioOrderItem[], + indexOfStartOrder: number, + unitPriceAtStartDate: Big, + averagePriceAtStartDate: Big, + totalUnits: Big, + totalInvestment: Big, + investmentAtStartDate: Big, + valueAtStartDate: Big, + maxTotalInvestment: Big, + averagePriceAtEndDate: Big, + initialValue: Big, + fees: Big, + indexOfEndOrder: number, + marketSymbolMap: { [date: string]: { [symbol: string]: Big } }, + grossPerformanceFromSells: Big, + totalInvestmentWithGrossPerformanceFromSell: Big, + lastAveragePrice: Big, + grossPerformance: Big, + feesAtStartDate: Big, + grossPerformanceAtStartDate: Big, + isChartMode: boolean, + currentValues: { [date: string]: Big }, + netPerformanceValues: { [date: string]: Big }, + netPerformanceValuesPercentage: { [date: string]: Big }, + investmentValues: { [date: string]: Big }, + maxInvestmentValues: { [date: string]: Big } + ) { + for (let i = 0; i < orders.length; i += 1) { + const order = orders[i]; + this.calculateNetPerformancePercentageForDateAndSymbol( + i, + orders, + order, + netPerformanceValuesPercentage, + marketSymbolMap ); - const valueOfInvestment = totalUnits.mul(order.unitPrice); - - const grossPerformanceFromSell = - order.type === TypeOfOrder.SELL - ? order.unitPrice.minus(lastAveragePrice).mul(order.quantity) - : new Big(0); + if (PortfolioCalculator.ENABLE_LOGGING) { + console.log(); + console.log(); + console.log(i + 1, order.type, order.itemType); + } - grossPerformanceFromSells = grossPerformanceFromSells.plus( - grossPerformanceFromSell + this.handleStartOrder( + order, + indexOfStartOrder, + orders, + i, + unitPriceAtStartDate ); - totalInvestmentWithGrossPerformanceFromSell = + // Calculate the average start price as soon as any units are held + let transactionInvestment; + let valueOfInvestment; + ({ + transactionInvestment, + valueOfInvestment, + averagePriceAtStartDate, + totalUnits, + totalInvestment, + investmentAtStartDate, + valueAtStartDate, + maxTotalInvestment, + averagePriceAtEndDate, + initialValue, + fees + } = this.calculateInvestmentSpecificMetrics( + averagePriceAtStartDate, + i, + indexOfStartOrder, + totalUnits, + totalInvestment, + order, + investmentAtStartDate, + valueAtStartDate, + maxTotalInvestment, + averagePriceAtEndDate, + indexOfEndOrder, + initialValue, + marketSymbolMap, + fees + )); + ({ + grossPerformanceFromSells, totalInvestmentWithGrossPerformanceFromSell - .plus(transactionInvestment) - .plus(grossPerformanceFromSell); + } = this.calculateSellOrders( + order, + lastAveragePrice, + grossPerformanceFromSells, + totalInvestmentWithGrossPerformanceFromSell, + transactionInvestment + )); lastAveragePrice = totalUnits.eq(0) ? new Big(0) : totalInvestmentWithGrossPerformanceFromSell.div(totalUnits); - if (PortfolioCalculator.ENABLE_LOGGING) { - console.log( - 'totalInvestmentWithGrossPerformanceFromSell', - totalInvestmentWithGrossPerformanceFromSell.toNumber() - ); - console.log( - 'grossPerformanceFromSells', - grossPerformanceFromSells.toNumber() - ); - } - const newGrossPerformance = valueOfInvestment .minus(totalInvestment) .plus(grossPerformanceFromSells); @@ -1247,82 +1524,418 @@ export class PortfolioCalculator { grossPerformanceAtStartDate = grossPerformance; } - if (isChartMode && i > indexOfStartOrder) { - currentValues[order.date] = valueOfInvestment; - netPerformanceValues[order.date] = grossPerformance - .minus(grossPerformanceAtStartDate) - .minus(fees.minus(feesAtStartDate)); - - investmentValues[order.date] = totalInvestment; - maxInvestmentValues[order.date] = maxTotalInvestment; - } + this.calculatePerformancesForDate( + isChartMode, + i, + indexOfStartOrder, + currentValues, + order, + valueOfInvestment, + netPerformanceValues, + grossPerformance, + grossPerformanceAtStartDate, + fees, + feesAtStartDate, + investmentValues, + totalInvestment, + maxInvestmentValues, + maxTotalInvestment + ); - if (PortfolioCalculator.ENABLE_LOGGING) { - console.log('totalInvestment', totalInvestment.toNumber()); - console.log( - 'totalGrossPerformance', - grossPerformance.minus(grossPerformanceAtStartDate).toNumber() - ); - } + this.handleLoggingOfInvestmentMetrics( + totalInvestment, + order, + transactionInvestment, + totalInvestmentWithGrossPerformanceFromSell, + grossPerformanceFromSells, + grossPerformance, + grossPerformanceAtStartDate + ); if (i === indexOfEndOrder) { break; } } + return { + lastAveragePrice, + grossPerformance, + feesAtStartDate, + grossPerformanceAtStartDate, + averagePriceAtStartDate, + totalUnits, + totalInvestment, + investmentAtStartDate, + valueAtStartDate, + maxTotalInvestment, + averagePriceAtEndDate, + initialValue, + fees, + netPerformanceValuesPercentage + }; + } - const totalGrossPerformance = grossPerformance.minus( - grossPerformanceAtStartDate - ); - - const totalNetPerformance = grossPerformance - .minus(grossPerformanceAtStartDate) - .minus(fees.minus(feesAtStartDate)); + private calculateNetPerformancePercentageForDateAndSymbol( + i: number, + orders: PortfolioOrderItem[], + order: PortfolioOrderItem, + netPerformanceValuesPercentage: { [date: string]: Big }, + marketSymbolMap: { [date: string]: { [symbol: string]: Big } } + ) { + if (i > 0 && order) { + const previousOrder = orders[i - 1]; + + if (order.unitPrice.toNumber() && previousOrder.unitPrice.toNumber()) { + netPerformanceValuesPercentage[order.date] = previousOrder.unitPrice + .div(order.unitPrice) + .minus(1); + } else if ( + order.type === 'STAKE' && + marketSymbolMap[order.date][order.symbol] + ) { + netPerformanceValuesPercentage[order.date] = + previousOrder.type === 'STAKE' + ? marketSymbolMap[previousOrder.date][previousOrder.symbol] + : previousOrder.unitPrice + .div(marketSymbolMap[order.date][order.symbol]) + .minus(1); + } else if (previousOrder.unitPrice.toNumber()) { + netPerformanceValuesPercentage[order.date] = new Big(-1); + } else if (previousOrder.type === 'STAKE' && order.unitPrice.toNumber()) { + netPerformanceValuesPercentage[order.date] = marketSymbolMap[ + previousOrder.date + ][previousOrder.symbol] + .div(order.unitPrice) + .minus(1); + } else { + netPerformanceValuesPercentage[order.date] = new Big(0); + } + } + } - const maxInvestmentBetweenStartAndEndDate = valueAtStartDate.plus( - maxTotalInvestment.minus(investmentAtStartDate) - ); + private handleLoggingOfInvestmentMetrics( + totalInvestment: Big, + order: PortfolioOrderItem, + transactionInvestment: any, + totalInvestmentWithGrossPerformanceFromSell: Big, + grossPerformanceFromSells: Big, + grossPerformance: Big, + grossPerformanceAtStartDate: Big + ) { + if (PortfolioCalculator.ENABLE_LOGGING) { + console.log('totalInvestment', totalInvestment.toNumber()); + console.log('order.quantity', order.quantity.toNumber()); + console.log('transactionInvestment', transactionInvestment.toNumber()); + console.log( + 'totalInvestmentWithGrossPerformanceFromSell', + totalInvestmentWithGrossPerformanceFromSell.toNumber() + ); + console.log( + 'grossPerformanceFromSells', + grossPerformanceFromSells.toNumber() + ); + console.log('totalInvestment', totalInvestment.toNumber()); + console.log( + 'totalGrossPerformance', + grossPerformance.minus(grossPerformanceAtStartDate).toNumber() + ); + } + } - const grossPerformancePercentage = - PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT || + private calculateNetPerformancePercentage( + averagePriceAtStartDate: Big, + averagePriceAtEndDate: Big, + orders: PortfolioOrderItem[], + indexOfStartOrder: number, + maxInvestmentBetweenStartAndEndDate: Big, + totalNetPerformance: Big, + unitPriceAtEndDate: Big, + feesPerUnit: Big + ) { + return PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT || averagePriceAtStartDate.eq(0) || averagePriceAtEndDate.eq(0) || orders[indexOfStartOrder].unitPrice.eq(0) - ? maxInvestmentBetweenStartAndEndDate.gt(0) - ? totalGrossPerformance.div(maxInvestmentBetweenStartAndEndDate) - : new Big(0) - : // This formula has the issue that buying more units with a price - // lower than the average buying price results in a positive - // performance even if the market price stays constant - unitPriceAtEndDate - .div(averagePriceAtEndDate) - .div( - orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate) - ) - .minus(1); - - const feesPerUnit = totalUnits.gt(0) - ? fees.minus(feesAtStartDate).div(totalUnits) - : new Big(0); + ? maxInvestmentBetweenStartAndEndDate.gt(0) + ? totalNetPerformance.div(maxInvestmentBetweenStartAndEndDate) + : new Big(0) + : // This formula has the issue that buying more units with a price + + // lower than the average buying price results in a positive + // performance even if the market price stays constant + unitPriceAtEndDate + .minus(feesPerUnit) + .div(averagePriceAtEndDate) + .div(orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)) + .minus(1); + } - const netPerformancePercentage = - PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT || + private calculateGrossPerformancePercentage( + averagePriceAtStartDate: Big, + averagePriceAtEndDate: Big, + orders: PortfolioOrderItem[], + indexOfStartOrder: number, + maxInvestmentBetweenStartAndEndDate: Big, + totalGrossPerformance: Big, + unitPriceAtEndDate: Big + ) { + return PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT || averagePriceAtStartDate.eq(0) || averagePriceAtEndDate.eq(0) || orders[indexOfStartOrder].unitPrice.eq(0) - ? maxInvestmentBetweenStartAndEndDate.gt(0) - ? totalNetPerformance.div(maxInvestmentBetweenStartAndEndDate) - : new Big(0) - : // This formula has the issue that buying more units with a price - // lower than the average buying price results in a positive - // performance even if the market price stays constant - unitPriceAtEndDate - .minus(feesPerUnit) - .div(averagePriceAtEndDate) - .div( - orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate) - ) - .minus(1); + ? maxInvestmentBetweenStartAndEndDate.gt(0) + ? totalGrossPerformance.div(maxInvestmentBetweenStartAndEndDate) + : new Big(0) + : // This formula has the issue that buying more units with a price + + // lower than the average buying price results in a positive + // performance even if the market price stays constant + unitPriceAtEndDate + .div(averagePriceAtEndDate) + .div(orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate)) + .minus(1); + } + + private calculateInvestmentSpecificMetrics( + averagePriceAtStartDate: Big, + i: number, + indexOfStartOrder: number, + totalUnits: Big, + totalInvestment: Big, + order: PortfolioOrderItem, + investmentAtStartDate: Big, + valueAtStartDate: Big, + maxTotalInvestment: Big, + averagePriceAtEndDate: Big, + indexOfEndOrder: number, + initialValue: Big, + marketSymbolMap: { [date: string]: { [symbol: string]: Big } }, + fees: Big + ) { + averagePriceAtStartDate = this.calculateAveragePrice( + averagePriceAtStartDate, + i, + indexOfStartOrder, + totalUnits, + totalInvestment + ); + + const valueOfInvestmentBeforeTransaction = totalUnits.mul(order.unitPrice); + if (!investmentAtStartDate && i >= indexOfStartOrder) { + investmentAtStartDate = totalInvestment ?? new Big(0); + valueAtStartDate = valueOfInvestmentBeforeTransaction; + } + + const transactionInvestment = this.getTransactionInvestment( + order, + totalUnits, + totalInvestment + ); + + totalInvestment = totalInvestment.plus(transactionInvestment); + + if (i >= indexOfStartOrder && totalInvestment.gt(maxTotalInvestment)) { + maxTotalInvestment = totalInvestment; + } + + averagePriceAtEndDate = this.calculateAveragePriceAtEnd( + i, + indexOfEndOrder, + totalUnits, + averagePriceAtEndDate, + totalInvestment + ); + + initialValue = this.calculateInitialValue( + i, + indexOfStartOrder, + initialValue, + valueOfInvestmentBeforeTransaction, + transactionInvestment, + order, + marketSymbolMap + ); + + fees = fees.plus(order.fee); + + totalUnits = totalUnits.plus( + order.quantity.mul(this.getFactor(order.type)) + ); + + const valueOfInvestment = totalUnits.mul(order.unitPrice); + return { + transactionInvestment, + valueOfInvestment, + averagePriceAtStartDate, + totalUnits, + totalInvestment, + investmentAtStartDate, + valueAtStartDate, + maxTotalInvestment, + averagePriceAtEndDate, + initialValue, + fees + }; + } + + private calculatePerformancesForDate( + isChartMode: boolean, + i: number, + indexOfStartOrder: number, + currentValues: { [date: string]: Big }, + order: PortfolioOrderItem, + valueOfInvestment: Big, + netPerformanceValues: { [date: string]: Big }, + grossPerformance: Big, + grossPerformanceAtStartDate: Big, + fees: Big, + feesAtStartDate: Big, + investmentValues: { [date: string]: Big }, + totalInvestment: Big, + maxInvestmentValues: { [date: string]: Big }, + maxTotalInvestment: Big + ) { + if (isChartMode && i > indexOfStartOrder) { + currentValues[order.date] = valueOfInvestment; + netPerformanceValues[order.date] = grossPerformance + .minus(grossPerformanceAtStartDate) + .minus(fees.minus(feesAtStartDate)); + + investmentValues[order.date] = totalInvestment; + maxInvestmentValues[order.date] = maxTotalInvestment; + } + } + + private calculateSellOrders( + order: PortfolioOrderItem, + lastAveragePrice: Big, + grossPerformanceFromSells: Big, + totalInvestmentWithGrossPerformanceFromSell: Big, + transactionInvestment: Big + ) { + const grossPerformanceFromSell = + order.type === TypeOfOrder.SELL + ? order.unitPrice.minus(lastAveragePrice).mul(order.quantity) + : new Big(0); + + grossPerformanceFromSells = grossPerformanceFromSells.plus( + grossPerformanceFromSell + ); + + totalInvestmentWithGrossPerformanceFromSell = + totalInvestmentWithGrossPerformanceFromSell + .plus(transactionInvestment) + .plus(grossPerformanceFromSell); + return { + grossPerformanceFromSells, + totalInvestmentWithGrossPerformanceFromSell + }; + } + + private calculateInitialValue( + i: number, + indexOfStartOrder: number, + initialValue: Big, + valueOfInvestmentBeforeTransaction: Big, + transactionInvestment: Big, + order: PortfolioOrderItem, + marketSymbolMap: { [date: string]: { [symbol: string]: Big } } + ) { + if (i >= indexOfStartOrder && !initialValue) { + if ( + i === indexOfStartOrder && + !valueOfInvestmentBeforeTransaction.eq(0) + ) { + initialValue = valueOfInvestmentBeforeTransaction; + } else if (transactionInvestment.gt(0)) { + initialValue = transactionInvestment; + } else if (order.type === 'STAKE') { + // For Parachain Rewards or Stock SpinOffs, first transactionInvestment might be 0 if the symbol has been acquired for free + initialValue = order.quantity.mul( + marketSymbolMap[order.date]?.[order.symbol] ?? new Big(0) + ); + } + } + return initialValue; + } + + private calculateAveragePriceAtEnd( + i: number, + indexOfEndOrder: number, + totalUnits: Big, + averagePriceAtEndDate: Big, + totalInvestment: Big + ) { + if (i === indexOfEndOrder && totalUnits.gt(0)) { + averagePriceAtEndDate = totalInvestment.div(totalUnits); + } + return averagePriceAtEndDate; + } + + private getTransactionInvestment( + order: PortfolioOrderItem, + totalUnits: Big, + totalInvestment: Big + ) { + return order.type === 'BUY' || order.type === 'STAKE' + ? order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type)) + : totalUnits.gt(0) + ? totalInvestment + .div(totalUnits) + .mul(order.quantity) + .mul(this.getFactor(order.type)) + : new Big(0); + } + + private calculateAveragePrice( + averagePriceAtStartDate: Big, + i: number, + indexOfStartOrder: number, + totalUnits: Big, + totalInvestment: Big + ) { + if ( + averagePriceAtStartDate.eq(0) && + i >= indexOfStartOrder && + totalUnits.gt(0) + ) { + averagePriceAtStartDate = totalInvestment.div(totalUnits); + } + return averagePriceAtStartDate; + } + + private handleStartOrder( + order: PortfolioOrderItem, + indexOfStartOrder: number, + orders: PortfolioOrderItem[], + i: number, + unitPriceAtStartDate: Big + ) { + 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 + : unitPriceAtStartDate; + } + } + + private handleLogging( + symbol: string, + orders: PortfolioOrderItem[], + indexOfStartOrder: number, + unitPriceAtEndDate: Big, + averagePriceAtStartDate: Big, + averagePriceAtEndDate: Big, + totalInvestment: Big, + maxTotalInvestment: Big, + totalGrossPerformance: Big, + grossPerformancePercentage: Big, + feesPerUnit: Big, + totalNetPerformance: Big, + netPerformancePercentage: Big + ) { if (PortfolioCalculator.ENABLE_LOGGING) { console.log( ` @@ -1344,19 +1957,132 @@ export class PortfolioCalculator { )} / ${netPerformancePercentage.mul(100).toFixed(2)}%` ); } + } - return { - currentValues, - grossPerformancePercentage, - initialValue, - investmentValues, - maxInvestmentValues, - netPerformancePercentage, - netPerformanceValues, - grossPerformance: totalGrossPerformance, - hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), - netPerformance: totalNetPerformance - }; + private sortOrdersByTime(orders: PortfolioOrderItem[]) { + orders = sortBy(orders, (order) => { + let sortIndex = new Date(order.date); + + if (order.itemType === 'start') { + sortIndex = addMilliseconds(sortIndex, -1); + } + + if (order.itemType === 'end') { + sortIndex = addMilliseconds(sortIndex, 1); + } + + return sortIndex.getTime(); + }); + return orders; + } + + private handleChartMode( + isChartMode: boolean, + orders: PortfolioOrderItem[], + day: Date, + end: Date, + symbol: string, + marketSymbolMap: { [date: string]: { [symbol: string]: Big } }, + lastUnitPrice: Big, + step: number + ) { + if (isChartMode) { + const datesWithOrders = {}; + + for (const order of orders) { + datesWithOrders[order.date] = true; + } + + while (isBefore(day, end)) { + this.handleDay( + datesWithOrders, + day, + orders, + symbol, + marketSymbolMap, + lastUnitPrice + ); + + lastUnitPrice = last(orders).unitPrice; + + day = addDays(day, step); + } + } + return { day, lastUnitPrice }; + } + + private handleDay( + datesWithOrders: {}, + day: Date, + orders: PortfolioOrderItem[], + symbol: string, + marketSymbolMap: { [date: string]: { [symbol: string]: Big } }, + lastUnitPrice: Big + ) { + const hasDate = datesWithOrders[format(day, DATE_FORMAT)]; + + if (!hasDate) { + orders.push({ + symbol, + currency: null, + date: format(day, DATE_FORMAT), + dataSource: null, + fee: new Big(0), + name: '', + quantity: new Big(0), + type: TypeOfOrder.BUY, + unitPrice: + marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? lastUnitPrice + }); + } else { + let orderIndex = orders.findIndex( + (o) => o.date === format(day, DATE_FORMAT) && o.type === 'STAKE' + ); + if (orderIndex >= 0) { + let order = orders[orderIndex]; + orders.splice(orderIndex, 1); + orders.push({ + ...order, + unitPrice: + marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? lastUnitPrice + }); + } + } + } + + private addSyntheticStartAndEndOrders( + orders: PortfolioOrderItem[], + symbol: string, + start: Date, + unitPriceAtStartDate: Big, + end: Date, + unitPriceAtEndDate: Big + ) { + orders.push({ + symbol, + currency: null, + date: format(start, DATE_FORMAT), + dataSource: null, + fee: new Big(0), + itemType: 'start', + name: '', + quantity: new Big(0), + type: TypeOfOrder.BUY, + unitPrice: unitPriceAtStartDate + }); + + orders.push({ + symbol, + currency: null, + date: format(end, DATE_FORMAT), + dataSource: null, + fee: new Big(0), + itemType: 'end', + name: '', + quantity: new Big(0), + type: TypeOfOrder.BUY, + unitPrice: unitPriceAtEndDate + }); } private isNextItemActive( diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index d5da0a333..d31ce8aa8 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -359,7 +359,8 @@ export class PortfolioController { @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', @Query('tags') filterByTags?: string, - @Query('withExcludedAccounts') withExcludedAccounts = false + @Query('withExcludedAccounts') withExcludedAccounts = false, + @Query('timeWeightedPerformance') calculateTimeWeightedPerformance = false ): Promise { const filters = this.apiService.buildFiltersFromQueryParams({ filterByAccounts, @@ -372,7 +373,8 @@ export class PortfolioController { filters, impersonationId, withExcludedAccounts, - userId: this.request.user.id + userId: this.request.user.id, + calculateTimeWeightedPerformance }); if ( diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 9bc585397..730a22c57 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1153,13 +1153,15 @@ export class PortfolioService { filters, impersonationId, userId, - withExcludedAccounts = false + withExcludedAccounts = false, + calculateTimeWeightedPerformance = false }: { dateRange?: DateRange; filters?: Filter[]; impersonationId: string; userId: string; withExcludedAccounts?: boolean; + calculateTimeWeightedPerformance?: boolean; }): Promise { userId = await this.getUserId(impersonationId, userId); const user = await this.userService.user({ id: userId }); @@ -1254,7 +1256,8 @@ export class PortfolioService { portfolioOrders, transactionPoints, userCurrency, - userId + userId, + calculateTimeWeightedPerformance }); const itemOfToday = items.find(({ date }) => { @@ -1463,7 +1466,8 @@ export class PortfolioService { portfolioOrders, transactionPoints, userCurrency, - userId + userId, + calculateTimeWeightedPerformance }: { dateRange?: DateRange; impersonationId: string; @@ -1471,6 +1475,7 @@ export class PortfolioService { transactionPoints: TransactionPoint[]; userCurrency: string; userId: string; + calculateTimeWeightedPerformance: boolean; }): Promise { if (transactionPoints.length === 0) { return { @@ -1503,7 +1508,8 @@ export class PortfolioService { const items = await portfolioCalculator.getChartData( startDate, endDate, - step + step, + calculateTimeWeightedPerformance ); return {