diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index d4fc49189..a20dd6a16 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -81,99 +81,6 @@ export class PortfolioCalculator { this.computeTransactionPoints(); } - private computeTransactionPoints() { - this.transactionPoints = []; - const symbols: { [symbol: string]: TransactionPointSymbol } = {}; - - let lastDate: string = null; - let lastTransactionPoint: TransactionPoint = null; - - for (const order of this.orders) { - const currentDate = order.date; - - let currentTransactionPointItem: TransactionPointSymbol; - const oldAccumulatedSymbol = symbols[order.SymbolProfile.symbol]; - - const factor = getFactor(order.type); - - if (oldAccumulatedSymbol) { - let investment = oldAccumulatedSymbol.investment; - - const newQuantity = order.quantity - .mul(factor) - .plus(oldAccumulatedSymbol.quantity); - - if (order.type === 'BUY') { - investment = oldAccumulatedSymbol.investment.plus( - order.quantity.mul(order.unitPrice) - ); - } else if (order.type === 'SELL') { - investment = oldAccumulatedSymbol.investment.minus( - order.quantity.mul(oldAccumulatedSymbol.averagePrice) - ); - } - - currentTransactionPointItem = { - investment, - averagePrice: newQuantity.gt(0) - ? investment.div(newQuantity) - : new Big(0), - currency: order.SymbolProfile.currency, - dataSource: order.SymbolProfile.dataSource, - dividend: new Big(0), - fee: order.fee.plus(oldAccumulatedSymbol.fee), - firstBuyDate: oldAccumulatedSymbol.firstBuyDate, - quantity: newQuantity, - symbol: order.SymbolProfile.symbol, - tags: order.tags, - transactionCount: oldAccumulatedSymbol.transactionCount + 1 - }; - } else { - currentTransactionPointItem = { - averagePrice: order.unitPrice, - currency: order.SymbolProfile.currency, - dataSource: order.SymbolProfile.dataSource, - dividend: new Big(0), - fee: order.fee, - firstBuyDate: order.date, - investment: order.unitPrice.mul(order.quantity).mul(factor), - quantity: order.quantity.mul(factor), - symbol: order.SymbolProfile.symbol, - tags: order.tags, - transactionCount: 1 - }; - } - - symbols[order.SymbolProfile.symbol] = currentTransactionPointItem; - - const items = lastTransactionPoint?.items ?? []; - - const newItems = items.filter( - (transactionPointItem) => - transactionPointItem.symbol !== order.SymbolProfile.symbol - ); - - newItems.push(currentTransactionPointItem); - - newItems.sort((a, b) => { - return a.symbol?.localeCompare(b.symbol); - }); - - if (lastDate !== currentDate || lastTransactionPoint === null) { - lastTransactionPoint = { - date: currentDate, - items: newItems - }; - - this.transactionPoints.push(lastTransactionPoint); - } else { - lastTransactionPoint.items = newItems; - } - - lastDate = currentDate; - } - } - public getAnnualizedPerformancePercent({ daysInMarket, netPerformancePercent @@ -191,10 +98,6 @@ export class PortfolioCalculator { return new Big(0); } - public getTransactionPoints(): TransactionPoint[] { - return this.transactionPoints; - } - public async getChartData({ end = new Date(Date.now()), start, @@ -258,7 +161,7 @@ export class PortfolioCalculator { await this.exchangeRateDataService.getExchangeRatesByCurrency({ currencies: uniq(Object.values(currencies)), endDate: endOfDay(end), - startDate: parseDate(this.transactionPoints?.[0]?.date), + startDate: this.getStartDate(), targetCurrency: this.currency }); @@ -562,7 +465,7 @@ export class PortfolioCalculator { await this.exchangeRateDataService.getExchangeRatesByCurrency({ currencies: uniq(Object.values(currencies)), endDate: endOfDay(endDate), - startDate: parseDate(this.transactionPoints?.[0]?.date), + startDate: this.getStartDate(), targetCurrency: this.currency }); @@ -856,6 +759,109 @@ export class PortfolioCalculator { }; } + public getStartDate() { + return this.transactionPoints.length > 0 + ? parseDate(this.transactionPoints[0].date) + : new Date(); + } + + public getTransactionPoints() { + return this.transactionPoints; + } + + private computeTransactionPoints() { + this.transactionPoints = []; + const symbols: { [symbol: string]: TransactionPointSymbol } = {}; + + let lastDate: string = null; + let lastTransactionPoint: TransactionPoint = null; + + for (const order of this.orders) { + const currentDate = order.date; + + let currentTransactionPointItem: TransactionPointSymbol; + const oldAccumulatedSymbol = symbols[order.SymbolProfile.symbol]; + + const factor = getFactor(order.type); + + if (oldAccumulatedSymbol) { + let investment = oldAccumulatedSymbol.investment; + + const newQuantity = order.quantity + .mul(factor) + .plus(oldAccumulatedSymbol.quantity); + + if (order.type === 'BUY') { + investment = oldAccumulatedSymbol.investment.plus( + order.quantity.mul(order.unitPrice) + ); + } else if (order.type === 'SELL') { + investment = oldAccumulatedSymbol.investment.minus( + order.quantity.mul(oldAccumulatedSymbol.averagePrice) + ); + } + + currentTransactionPointItem = { + investment, + averagePrice: newQuantity.gt(0) + ? investment.div(newQuantity) + : new Big(0), + currency: order.SymbolProfile.currency, + dataSource: order.SymbolProfile.dataSource, + dividend: new Big(0), + fee: order.fee.plus(oldAccumulatedSymbol.fee), + firstBuyDate: oldAccumulatedSymbol.firstBuyDate, + quantity: newQuantity, + symbol: order.SymbolProfile.symbol, + tags: order.tags, + transactionCount: oldAccumulatedSymbol.transactionCount + 1 + }; + } else { + currentTransactionPointItem = { + averagePrice: order.unitPrice, + currency: order.SymbolProfile.currency, + dataSource: order.SymbolProfile.dataSource, + dividend: new Big(0), + fee: order.fee, + firstBuyDate: order.date, + investment: order.unitPrice.mul(order.quantity).mul(factor), + quantity: order.quantity.mul(factor), + symbol: order.SymbolProfile.symbol, + tags: order.tags, + transactionCount: 1 + }; + } + + symbols[order.SymbolProfile.symbol] = currentTransactionPointItem; + + const items = lastTransactionPoint?.items ?? []; + + const newItems = items.filter( + (transactionPointItem) => + transactionPointItem.symbol !== order.SymbolProfile.symbol + ); + + newItems.push(currentTransactionPointItem); + + newItems.sort((a, b) => { + return a.symbol?.localeCompare(b.symbol); + }); + + if (lastDate !== currentDate || lastTransactionPoint === null) { + lastTransactionPoint = { + date: currentDate, + items: newItems + }; + + this.transactionPoints.push(lastTransactionPoint); + } else { + lastTransactionPoint.items = newItems; + } + + lastDate = currentDate; + } + } + private getSymbolMetrics({ dataSource, end, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 55e886915..aac2fdbcc 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -266,14 +266,14 @@ export class PortfolioService { }): Promise { const userId = await this.getUserId(impersonationId, this.request.user.id); - const { activities, transactionPoints } = await this.getTransactionPoints({ + const { activities } = await this.getTransactionPoints({ filters, userId, includeDrafts: true, types: ['BUY', 'SELL'] }); - if (transactionPoints.length === 0) { + if (activities.length === 0) { return { investments: [], streaks: { currentStreak: 0, longestStreak: 0 } @@ -291,7 +291,6 @@ export class PortfolioService { dateRange, impersonationId, portfolioCalculator, - transactionPoints, userId, withDataDecimation: false }); @@ -362,7 +361,7 @@ export class PortfolioService { }); } - const { activities, transactionPoints } = await this.getTransactionPoints({ + const { activities } = await this.getTransactionPoints({ filters, types, userId, @@ -376,10 +375,10 @@ export class PortfolioService { exchangeRateDataService: this.exchangeRateDataService }); - const portfolioStart = parseDate( - transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT) + const startDate = this.getStartDate( + dateRange, + portfolioCalculator.getStartDate() ); - const startDate = this.getStartDate(dateRange, portfolioStart); const currentPositions = await portfolioCalculator.getCurrentPositions(startDate); @@ -962,13 +961,13 @@ export class PortfolioService { const userId = await this.getUserId(impersonationId, this.request.user.id); const user = await this.userService.user({ id: userId }); - const { activities, transactionPoints } = await this.getTransactionPoints({ + const { activities } = await this.getTransactionPoints({ filters, userId, types: ['BUY', 'SELL'] }); - if (transactionPoints?.length <= 0) { + if (activities?.length <= 0) { return { hasErrors: false, positions: [] @@ -982,8 +981,10 @@ export class PortfolioService { exchangeRateDataService: this.exchangeRateDataService }); - const portfolioStart = parseDate(transactionPoints[0].date); - const startDate = this.getStartDate(dateRange, portfolioStart); + const startDate = this.getStartDate( + dateRange, + portfolioCalculator.getStartDate() + ); const currentPositions = await portfolioCalculator.getCurrentPositions(startDate); @@ -1132,14 +1133,14 @@ export class PortfolioService { ) ); - const { activities, transactionPoints } = await this.getTransactionPoints({ + const { activities } = await this.getTransactionPoints({ filters, userId, withExcludedAccounts, types: withItems ? ['BUY', 'ITEM', 'SELL'] : ['BUY', 'SELL'] }); - if (accountBalanceItems?.length <= 0 && transactionPoints?.length <= 0) { + if (accountBalanceItems?.length <= 0 && activities?.length <= 0) { return { chart: [], firstOrderDate: undefined, @@ -1170,7 +1171,7 @@ export class PortfolioService { const portfolioStart = min( [ parseDate(accountBalanceItems[0]?.date), - parseDate(transactionPoints[0]?.date) + portfolioCalculator.getStartDate() ].filter((date) => { return isValid(date); }) @@ -1206,7 +1207,6 @@ export class PortfolioService { dateRange, impersonationId, portfolioCalculator, - transactionPoints, userId }); @@ -1283,7 +1283,7 @@ export class PortfolioService { const user = await this.userService.user({ id: userId }); const userCurrency = this.getUserCurrency(user); - const { activities, transactionPoints } = await this.getTransactionPoints({ + const { activities } = await this.getTransactionPoints({ userId, types: ['BUY', 'SELL'] }); @@ -1295,11 +1295,9 @@ export class PortfolioService { exchangeRateDataService: this.exchangeRateDataService }); - const portfolioStart = parseDate( - transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT) + const currentPositions = await portfolioCalculator.getCurrentPositions( + portfolioCalculator.getStartDate() ); - const currentPositions = - await portfolioCalculator.getCurrentPositions(portfolioStart); const positions = currentPositions.positions.filter( (item) => !item.quantity.eq(0) @@ -1429,18 +1427,16 @@ export class PortfolioService { dateRange = 'max', impersonationId, portfolioCalculator, - transactionPoints, userId, withDataDecimation = true }: { dateRange?: DateRange; impersonationId: string; portfolioCalculator: PortfolioCalculator; - transactionPoints: TransactionPoint[]; userId: string; withDataDecimation?: boolean; }): Promise { - if (transactionPoints.length === 0) { + if (portfolioCalculator.getTransactionPoints().length === 0) { return { isAllTimeHigh: false, isAllTimeLow: false, @@ -1450,8 +1446,10 @@ export class PortfolioService { userId = await this.getUserId(impersonationId, userId); - const portfolioStart = parseDate(transactionPoints[0].date); - const startDate = this.getStartDate(dateRange, portfolioStart); + const startDate = this.getStartDate( + dateRange, + portfolioCalculator.getStartDate() + ); const endDate = new Date(); const daysInMarket = differenceInDays(endDate, startDate) + 1; const step = withDataDecimation @@ -1944,12 +1942,11 @@ export class PortfolioService { withExcludedAccounts?: boolean; }): Promise<{ activities: Activity[]; - transactionPoints: TransactionPoint[]; }> { const userCurrency = this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY; - const { activities, count } = await this.orderService.getOrders({ + const { activities } = await this.orderService.getOrders({ filters, includeDrafts, types, @@ -1958,34 +1955,8 @@ export class PortfolioService { withExcludedAccounts }); - if (count <= 0) { - return { activities: [], transactionPoints: [] }; - } - - const portfolioOrders: PortfolioOrder[] = activities.map((order) => ({ - // currency: order.SymbolProfile.currency, - // dataSource: order.SymbolProfile.dataSource, - date: format(order.date, DATE_FORMAT), - fee: new Big(order.fee), - // name: order.SymbolProfile?.name, - quantity: new Big(order.quantity), - // symbol: order.SymbolProfile.symbol, - SymbolProfile: order.SymbolProfile, - // tags: order.tags, - type: order.type, - unitPrice: new Big(order.unitPrice) - })); - - const portfolioCalculator = new PortfolioCalculator({ - activities, - currency: userCurrency, - currentRateService: this.currentRateService, - exchangeRateDataService: this.exchangeRateDataService - }); - return { - activities, - transactionPoints: portfolioCalculator.getTransactionPoints() + activities }; }