diff --git a/CHANGELOG.md b/CHANGELOG.md index 8584582a3..36a4eff54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- Fixed an issue in the calculation of the portfolio summary caused by future liabilities + ## 2.77.1 - 2024-04-27 ### Added diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index 4a85dce57..bf4920463 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -88,21 +88,26 @@ export class OrderController { @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, - @Query('range') dateRange: DateRange = 'max', + @Query('range') dateRange?: DateRange, @Query('skip') skip?: number, @Query('sortColumn') sortColumn?: string, @Query('sortDirection') sortDirection?: Prisma.SortOrder, @Query('tags') filterByTags?: string, @Query('take') take?: number ): Promise { + let endDate: Date; + let startDate: Date; + + if (dateRange) { + ({ endDate, startDate } = getInterval(dateRange)); + } + const filters = this.apiService.buildFiltersFromQueryParams({ filterByAccounts, filterByAssetClasses, filterByTags }); - const { endDate, startDate } = getInterval(dateRange); - const impersonationUserId = await this.impersonationService.validateImpersonationId(impersonationId); const userCurrency = this.request.user.Settings.settings.baseCurrency; diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index ba9833948..568969603 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -37,6 +37,7 @@ import { eachDayOfInterval, endOfDay, format, + isAfter, isBefore, isSameDay, max, @@ -44,13 +45,12 @@ import { subDays } from 'date-fns'; import { first, last, uniq, uniqBy } from 'lodash'; -import ms from 'ms'; export abstract class PortfolioCalculator { protected static readonly ENABLE_LOGGING = false; protected accountBalanceItems: HistoricalDataItem[]; - protected orders: PortfolioOrder[]; + protected activities: PortfolioOrder[]; private configurationService: ConfigurationService; private currency: string; @@ -96,7 +96,7 @@ export abstract class PortfolioCalculator { this.exchangeRateDataService = exchangeRateDataService; this.isExperimentalFeatures = isExperimentalFeatures; - this.orders = activities + this.activities = activities .map( ({ date, @@ -107,6 +107,12 @@ export abstract class PortfolioCalculator { type, unitPrice }) => { + if (isAfter(date, new Date(Date.now()))) { + // Adapt date to today if activity is in future (e.g. liability) + // to include it in the interval + date = endOfDay(new Date(Date.now())); + } + return { SymbolProfile, tags, @@ -917,7 +923,7 @@ export abstract class PortfolioCalculator { tags, type, unitPrice - } of this.orders) { + } of this.activities) { let currentTransactionPointItem: TransactionPointSymbol; const oldAccumulatedSymbol = symbols[SymbolProfile.symbol]; diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts index fc858f293..3ae63c72a 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts @@ -74,7 +74,7 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2022-01-01'), + date: new Date('2023-01-01'), // Date in future fee: 0, quantity: 1, SymbolProfile: { @@ -96,61 +96,12 @@ describe('PortfolioCalculator', () => { userId: userDummyData.id }); - const portfolioSnapshot = await portfolioCalculator.computeSnapshot( - parseDate('2022-01-01') - ); - spy.mockRestore(); - expect(portfolioSnapshot).toEqual({ - currentValueInBaseCurrency: new Big('0'), - errors: [], - grossPerformance: new Big('0'), - grossPerformancePercentage: new Big('0'), - grossPerformancePercentageWithCurrencyEffect: new Big('0'), - grossPerformanceWithCurrencyEffect: new Big('0'), - hasErrors: true, - netPerformance: new Big('0'), - netPerformancePercentage: new Big('0'), - netPerformancePercentageWithCurrencyEffect: new Big('0'), - netPerformanceWithCurrencyEffect: new Big('0'), - positions: [ - { - averagePrice: new Big('3000'), - currency: 'USD', - dataSource: 'MANUAL', - dividend: new Big('0'), - dividendInBaseCurrency: new Big('0'), - fee: new Big('0'), - firstBuyDate: '2022-01-01', - grossPerformance: null, - grossPerformancePercentage: null, - grossPerformancePercentageWithCurrencyEffect: null, - grossPerformanceWithCurrencyEffect: null, - investment: new Big('0'), - investmentWithCurrencyEffect: new Big('0'), - marketPrice: null, - marketPriceInBaseCurrency: 3000, - netPerformance: null, - netPerformancePercentage: null, - netPerformancePercentageWithCurrencyEffect: null, - netPerformanceWithCurrencyEffect: null, - quantity: new Big('0'), - symbol: '55196015-1365-4560-aa60-8751ae6d18f8', - tags: [], - timeWeightedInvestment: new Big('0'), - timeWeightedInvestmentWithCurrencyEffect: new Big('0'), - transactionCount: 1, - valueInBaseCurrency: new Big('0') - } - ], - totalFeesWithCurrencyEffect: new Big('0'), - totalInterestWithCurrencyEffect: new Big('0'), - totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0'), - totalLiabilitiesWithCurrencyEffect: new Big('0'), - totalValuablesWithCurrencyEffect: new Big('0') - }); + const liabilitiesInBaseCurrency = + await portfolioCalculator.getLiabilitiesInBaseCurrency(); + + expect(liabilitiesInBaseCurrency).toEqual(new Big(3000)); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index f480fdc59..f8b62a940 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -203,7 +203,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { let valueAtStartDateWithCurrencyEffect: Big; // Clone orders to keep the original values in this.orders - let orders: PortfolioOrderItem[] = cloneDeep(this.orders).filter( + let orders: PortfolioOrderItem[] = cloneDeep(this.activities).filter( ({ SymbolProfile }) => { return SymbolProfile.symbol === symbol; } diff --git a/apps/api/src/app/portfolio/current-rate.service.mock.ts b/apps/api/src/app/portfolio/current-rate.service.mock.ts index ed9229691..8ac1d15bd 100644 --- a/apps/api/src/app/portfolio/current-rate.service.mock.ts +++ b/apps/api/src/app/portfolio/current-rate.service.mock.ts @@ -8,6 +8,13 @@ import { GetValuesParams } from './interfaces/get-values-params.interface'; function mockGetValue(symbol: string, date: Date) { switch (symbol) { + case '55196015-1365-4560-aa60-8751ae6d18f8': + if (isSameDay(parseDate('2022-01-31'), date)) { + return { marketPrice: 3000 }; + } + + return { marketPrice: 0 }; + case 'BALN.SW': if (isSameDay(parseDate('2021-11-12'), date)) { return { marketPrice: 146 };