diff --git a/CHANGELOG.md b/CHANGELOG.md index 220b29036..8d8da684b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added a close holding button to the holding detail dialog +- Extended the holdings endpoint to include the performance with currency effect for cash - Extended the user detail dialog in the users section of the admin control panel ### Changed diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b74b779f6..eaebed09e 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -319,6 +319,92 @@ export class PortfolioService { }; } + public async getCashActivities({ + cashDetails, + userCurrency, + userId + }: { + cashDetails: CashDetails; + userCurrency: string; + userId: string; + }) { + const syntheticActivities: Activity[] = []; + + for (const account of cashDetails.accounts) { + const { balances } = await this.accountBalanceService.getAccountBalances({ + filters: [{ id: account.id, type: 'ACCOUNT' }], + userCurrency, + userId + }); + + let currentBalance = 0; + let currentBalanceInBaseCurrency = 0; + for (const balanceItem of balances) { + const syntheticActivityTemplate: Activity = { + userId, + accountId: account.id, + accountUserId: account.userId, + comment: account.name, + createdAt: new Date(balanceItem.date), + currency: account.currency, + date: new Date(balanceItem.date), + fee: 0, + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + id: balanceItem.id, + isDraft: false, + quantity: 1, + SymbolProfile: { + activitiesCount: 0, + assetClass: 'LIQUIDITY', + assetSubClass: 'CASH', + countries: [], + createdAt: new Date(balanceItem.date), + currency: account.currency, + dataSource: 'YAHOO', + holdings: [], + id: account.currency, + isActive: true, + sectors: [], + symbol: account.currency, + updatedAt: new Date(balanceItem.date) + }, + symbolProfileId: account.currency, + type: 'BUY', + unitPrice: 1, + unitPriceInAssetProfileCurrency: 1, + updatedAt: new Date(balanceItem.date), + valueInBaseCurrency: 0, + value: 0 + }; + + if (currentBalance < balanceItem.value) { + // BUY + syntheticActivities.push({ + ...syntheticActivityTemplate, + type: 'BUY', + value: balanceItem.value - currentBalance, + valueInBaseCurrency: + balanceItem.valueInBaseCurrency - currentBalanceInBaseCurrency + }); + } else if (currentBalance > balanceItem.value) { + // SELL + syntheticActivities.push({ + ...syntheticActivityTemplate, + type: 'SELL', + value: currentBalance - balanceItem.value, + valueInBaseCurrency: + currentBalanceInBaseCurrency - balanceItem.valueInBaseCurrency + }); + } + currentBalance = balanceItem.value; + currentBalanceInBaseCurrency = balanceItem.valueInBaseCurrency; + } + } + + return syntheticActivities; + } + public async getDividends({ activities, groupBy @@ -488,6 +574,7 @@ export class PortfolioService { (user.settings?.settings as UserSettings)?.emergencyFund ?? 0 ); + // Activities for non-cash assets const { activities } = await this.orderService.getOrdersForPortfolioCalculator({ filters, @@ -495,23 +582,29 @@ export class PortfolioService { userId }); - const portfolioCalculator = this.calculatorFactory.createCalculator({ - activities, + // Synthetic activities for cash + const cashDetails = await this.accountService.getCashDetails({ filters, userId, - calculationType: this.getUserPerformanceCalculationType(user), currency: userCurrency }); + const cashActivities = await this.getCashActivities({ + cashDetails, + userCurrency, + userId + }); - const { createdAt, currentValueInBaseCurrency, hasErrors, positions } = - await portfolioCalculator.getSnapshot(); - - const cashDetails = await this.accountService.getCashDetails({ + const portfolioCalculator = this.calculatorFactory.createCalculator({ filters, userId, + activities: [...activities, ...cashActivities], + calculationType: this.getUserPerformanceCalculationType(user), currency: userCurrency }); + const { createdAt, currentValueInBaseCurrency, hasErrors, positions } = + await portfolioCalculator.getSnapshot(); + const holdings: PortfolioDetails['holdings'] = {}; const totalValueInBaseCurrency = currentValueInBaseCurrency.plus( diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index 47c67c3de..6d2e18de3 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -26,6 +26,8 @@ import { import { isNumber } from 'lodash'; import ms from 'ms'; +import { ExchangeRatesByCurrency } from './interfaces/exchange-rate-data.interface'; + @Injectable() export class ExchangeRateDataService { private currencies: string[] = []; @@ -58,7 +60,7 @@ export class ExchangeRateDataService { endDate?: Date; startDate: Date; targetCurrency: string; - }) { + }): Promise { if (!startDate) { return {}; } diff --git a/apps/api/src/services/exchange-rate-data/interfaces/exchange-rate-data.interface.ts b/apps/api/src/services/exchange-rate-data/interfaces/exchange-rate-data.interface.ts new file mode 100644 index 000000000..8e0d2c0d4 --- /dev/null +++ b/apps/api/src/services/exchange-rate-data/interfaces/exchange-rate-data.interface.ts @@ -0,0 +1,5 @@ +export interface ExchangeRatesByCurrency { + [currency: string]: { + [dateString: string]: number; + }; +}