From 1f380a91c3c663547e431795bf80ff1650410e74 Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 2 Mar 2024 12:59:32 +0100 Subject: [PATCH 1/4] Added Performance Logging Interceptor --- apps/api/src/aop/logging.interceptor.ts | 55 +++++++++++++++++++ .../account-balance.service.ts | 2 + apps/api/src/main.ts | 2 + .../data-provider/data-provider.service.ts | 2 + .../symbol-profile/symbol-profile.service.ts | 2 + 5 files changed, 63 insertions(+) create mode 100644 apps/api/src/aop/logging.interceptor.ts diff --git a/apps/api/src/aop/logging.interceptor.ts b/apps/api/src/aop/logging.interceptor.ts new file mode 100644 index 000000000..b98c3d31b --- /dev/null +++ b/apps/api/src/aop/logging.interceptor.ts @@ -0,0 +1,55 @@ +import { Logger } from '@nestjs/common'; +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +const dict = {}; + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const methodName = + context.getClass().name + ':' + context.getHandler().name; + Logger.debug(`Before ${methodName}...`); + + const now = Date.now(); + return next + .handle() + .pipe( + tap(() => Logger.debug(`After ${methodName}... ${Date.now() - now}ms`)) + ); + } +} + +export function LogPerformance( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor +) { + const originalMethod = descriptor.value; + if (Object.keys(dict).includes(propertyKey)) { + dict[propertyKey] += 1; + } else { + dict[propertyKey] = 1; + } + + descriptor.value = function (...args: any[]) { + const time = Date.now(); + const result = originalMethod.apply(this, args); + const now = Date.now(); + if (now - time > 100) { + Logger.debug(`${propertyKey} returned within: ${now - time} ms`); + } else if (dict[propertyKey] > 100) { + Logger.debug(`${propertyKey} was called the 100th time`); + dict[propertyKey] = 0; + } + return result; + }; + + return descriptor; +} diff --git a/apps/api/src/app/account-balance/account-balance.service.ts b/apps/api/src/app/account-balance/account-balance.service.ts index 8a9d7b83e..a75a1cbd1 100644 --- a/apps/api/src/app/account-balance/account-balance.service.ts +++ b/apps/api/src/app/account-balance/account-balance.service.ts @@ -1,3 +1,4 @@ +import { LogPerformance } from '@ghostfolio/api/aop/logging.interceptor'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces'; @@ -40,6 +41,7 @@ export class AccountBalanceService { }); } + @LogPerformance public async getAccountBalances({ filters, user, diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 016f82473..b7b9a548a 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -5,6 +5,7 @@ import type { NestExpressApplication } from '@nestjs/platform-express'; import * as bodyParser from 'body-parser'; import helmet from 'helmet'; +import { LoggingInterceptor } from './aop/logging.interceptor'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; import { HtmlTemplateMiddleware } from './middlewares/html-template.middleware'; @@ -25,6 +26,7 @@ async function bootstrap() { type: VersioningType.URI }); app.setGlobalPrefix('api', { exclude: ['sitemap.xml'] }); + app.useGlobalInterceptors(new LoggingInterceptor()); app.useGlobalPipes( new ValidationPipe({ forbidNonWhitelisted: true, diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index f8aca3723..1c951f4d7 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -1,3 +1,4 @@ +import { LogPerformance } from '@ghostfolio/api/aop/logging.interceptor'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; @@ -332,6 +333,7 @@ export class DataProviderService { return result; } + @LogPerformance public async getQuotes({ items, requestTimeout, diff --git a/apps/api/src/services/symbol-profile/symbol-profile.service.ts b/apps/api/src/services/symbol-profile/symbol-profile.service.ts index ce5337c17..abf973029 100644 --- a/apps/api/src/services/symbol-profile/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile/symbol-profile.service.ts @@ -1,3 +1,4 @@ +import { LogPerformance } from '@ghostfolio/api/aop/logging.interceptor'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { @@ -39,6 +40,7 @@ export class SymbolProfileService { }); } + @LogPerformance public async getSymbolProfiles( aUniqueAssets: UniqueAsset[] ): Promise { From bffbc4c1c7cb16bd7a08b4fb510c11dd54e461b0 Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 2 Mar 2024 12:59:54 +0100 Subject: [PATCH 2/4] Added Performance Interceptor and removed Caculation from asset page --- .../src/app/portfolio/portfolio-calculator.ts | 162 +++++++++++++++--- .../src/app/portfolio/portfolio.controller.ts | 8 +- .../src/app/portfolio/portfolio.service.ts | 71 ++++++-- .../allocations/allocations-page.component.ts | 5 +- apps/client/src/app/services/data.service.ts | 16 +- 5 files changed, 217 insertions(+), 45 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio-calculator.ts b/apps/api/src/app/portfolio/portfolio-calculator.ts index 01369743b..00b0b522b 100644 --- a/apps/api/src/app/portfolio/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/portfolio-calculator.ts @@ -1,3 +1,4 @@ +import { LogPerformance } from '@ghostfolio/api/aop/logging.interceptor'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; @@ -80,6 +81,7 @@ export class PortfolioCalculator { }); } + @LogPerformance public computeTransactionPoints() { this.transactionPoints = []; const symbols: { [symbol: string]: TransactionPointSymbol } = {}; @@ -123,6 +125,7 @@ export class PortfolioCalculator { } } + @LogPerformance private getCurrentTransactionPointItem( oldAccumulatedSymbol: TransactionPointSymbol, order: PortfolioOrder, @@ -154,6 +157,7 @@ export class PortfolioCalculator { return currentTransactionPointItem; } + @LogPerformance private handleSubsequentTransactions( order: PortfolioOrder, factor: number, @@ -196,6 +200,7 @@ export class PortfolioCalculator { return currentTransactionPointItem; } + @LogPerformance public getAnnualizedPerformancePercent({ daysInMarket, netPerformancePercent @@ -221,6 +226,7 @@ export class PortfolioCalculator { this.transactionPoints = transactionPoints; } + @LogPerformance public async getChartData({ start, end = new Date(Date.now()), @@ -345,6 +351,7 @@ export class PortfolioCalculator { }); } + @LogPerformance private calculatePerformance( date: Date, previousDate: Date, @@ -479,6 +486,7 @@ export class PortfolioCalculator { }; } + @LogPerformance private accumulatedValuesByDate( valuesBySymbol: { [symbol: string]: { @@ -587,6 +595,7 @@ export class PortfolioCalculator { return accumulatedValuesByDate; } + @LogPerformance private populateSymbolMetrics( symbols: { [symbol: string]: boolean }, end: Date, @@ -648,6 +657,7 @@ export class PortfolioCalculator { } } + @LogPerformance private populateMarketSymbolMap( marketSymbols: GetValueObject[], marketSymbolMap: { [date: string]: { [symbol: string]: Big } } @@ -665,6 +675,7 @@ export class PortfolioCalculator { } } + @LogPerformance private async getInformationFromCurrentRateService( currencies: { [symbol: string]: string }, dataGatheringItems: IDataGatheringItem[], @@ -681,6 +692,7 @@ export class PortfolioCalculator { }); } + @LogPerformance private pushDataGatheringsSymbols( transactionPointsBeforeEndDate: TransactionPoint[], firstIndex: number, @@ -700,6 +712,7 @@ export class PortfolioCalculator { } } + @LogPerformance private getRelevantStartAndEndDates( start: Date, end: Date, @@ -718,9 +731,11 @@ export class PortfolioCalculator { } } + @LogPerformance public async getCurrentPositions( start: Date, - end = new Date(Date.now()) + end = new Date(Date.now()), + calculatePerformance = true ): Promise { const transactionPointsBeforeEndDate = this.transactionPoints?.filter((transactionPoint) => { @@ -883,7 +898,8 @@ export class PortfolioCalculator { start, exchangeRates: exchangeRatesByCurrency[`${item.currency}${this.currency}`], - symbol: item.symbol + symbol: item.symbol, + calculatePerformance }); hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; @@ -956,6 +972,7 @@ export class PortfolioCalculator { return this.dataProviderInfos; } + @LogPerformance public getInvestments(): { date: string; investment: Big }[] { if (this.transactionPoints.length === 0) { return []; @@ -973,6 +990,7 @@ export class PortfolioCalculator { }); } + @LogPerformance public getInvestmentsByGroup({ data, groupBy @@ -996,6 +1014,7 @@ export class PortfolioCalculator { })); } + @LogPerformance private calculateOverallPerformance(positions: TimelinePosition[]) { let currentValue = new Big(0); let grossPerformance = new Big(0); @@ -1121,6 +1140,7 @@ export class PortfolioCalculator { return factor; } + @LogPerformance private getSymbolMetrics({ end, exchangeRates, @@ -1128,7 +1148,8 @@ export class PortfolioCalculator { marketSymbolMap, start, step = 1, - symbol + symbol, + calculatePerformance = true }: { end: Date; exchangeRates: { [dateString: string]: number }; @@ -1139,6 +1160,7 @@ export class PortfolioCalculator { start: Date; step?: number; symbol: string; + calculatePerformance?: boolean; }): SymbolMetrics { const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; const currentValues: WithCurrencyEffect<{ [date: string]: Big }> = { @@ -1359,7 +1381,8 @@ export class PortfolioCalculator { unitPriceAtEndDate, symbol, exchangeRates, - currentExchangeRate + currentExchangeRate, + calculatePerformance ); return { @@ -1403,6 +1426,7 @@ export class PortfolioCalculator { }; } + @LogPerformance private calculatePerformanceOfSymbol( orders: PortfolioOrderItem[], indexOfStartOrder: number, @@ -1435,7 +1459,8 @@ export class PortfolioCalculator { unitPriceAtEndDate: Big, symbol: string, exchangeRates: { [dateString: string]: number }, - currentExchangeRate: number + currentExchangeRate: number, + calculatePerformance: boolean ) { let totalInvestmentDays = 0; let sumOfTimeWeightedInvestments = { @@ -1490,9 +1515,51 @@ export class PortfolioCalculator { sumOfTimeWeightedInvestments, timeWeightedInvestmentValues, exchangeRates, - currentExchangeRate + currentExchangeRate, + calculatePerformance )); + if (!calculatePerformance) { + return { + currentValues, + grossPerformancePercentage: { + Value: new Big(0), + WithCurrencyEffect: new Big(0) + }, + initialValue, + investmentValues, + maxInvestmentValues, + netPerformancePercentage: { + Value: new Big(0), + WithCurrencyEffect: new Big(0) + }, + netPerformanceValues, + grossPerformance: { Value: new Big(0), WithCurrencyEffect: new Big(0) }, + hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), + netPerformance: { Value: new Big(0), WithCurrencyEffect: new Big(0) }, + averagePriceAtStartDate, + totalUnits, + totalInvestment, + investmentAtStartDate, + valueAtStartDate, + maxTotalInvestment, + averagePriceAtEndDate, + fees, + lastAveragePrice, + grossPerformanceFromSells, + totalInvestmentWithGrossPerformanceFromSell, + feesAtStartDate, + grossPerformanceAtStartDate, + netPerformanceValuesPercentage, + investmentValuesAccumulated, + timeWeightedInvestmentValues, + timeWeightedAverageInvestmentBetweenStartAndEndDate: { + Value: new Big(0), + WithCurrencyEffect: new Big(0) + } + }; + } + const totalGrossPerformance = { Value: grossPerformance.Value.minus(grossPerformanceAtStartDate.Value), WithCurrencyEffect: grossPerformance.WithCurrencyEffect.minus( @@ -1601,6 +1668,7 @@ export class PortfolioCalculator { }; } + @LogPerformance private handleOrders( orders: PortfolioOrderItem[], indexOfStartOrder: number, @@ -1632,18 +1700,21 @@ export class PortfolioCalculator { sumOfTimeWeightedInvestments: WithCurrencyEffect, timeWeightedInvestmentValues: WithCurrencyEffect<{ [date: string]: Big }>, exchangeRates: { [dateString: string]: number }, - currentExchangeRate: number + currentExchangeRate: number, + calculatePerformance: boolean ) { for (let i = 0; i < orders.length; i += 1) { const order = orders[i]; const previousOrderDateString = i > 0 ? orders[i - 1].date : ''; - this.calculateNetPerformancePercentageForDateAndSymbol( - i, - orders, - order, - netPerformanceValuesPercentage, - marketSymbolMap - ); + if (calculatePerformance) { + this.calculateNetPerformancePercentageForDateAndSymbol( + i, + orders, + order, + netPerformanceValuesPercentage, + marketSymbolMap + ); + } if (PortfolioCalculator.ENABLE_LOGGING) { console.log(); @@ -1704,16 +1775,18 @@ export class PortfolioCalculator { fees )); - ({ - grossPerformanceFromSells, - totalInvestmentWithGrossPerformanceFromSell - } = this.calculateSellOrders( - order, - lastAveragePrice, - grossPerformanceFromSells, - totalInvestmentWithGrossPerformanceFromSell, - transactionInvestment - )); + if (calculatePerformance) { + ({ + grossPerformanceFromSells, + totalInvestmentWithGrossPerformanceFromSell + } = this.calculateSellOrders( + order, + lastAveragePrice, + grossPerformanceFromSells, + totalInvestmentWithGrossPerformanceFromSell, + transactionInvestment + )); + } lastAveragePrice.Value = totalUnits.eq(0) ? new Big(0) @@ -1744,6 +1817,26 @@ export class PortfolioCalculator { ); } + if (!calculatePerformance) { + return { + lastAveragePrice, + grossPerformance, + feesAtStartDate, + grossPerformanceAtStartDate, + averagePriceAtStartDate, + totalUnits, + totalInvestment, + investmentAtStartDate, + valueAtStartDate, + maxTotalInvestment, + averagePriceAtEndDate, + initialValue, + fees, + netPerformanceValuesPercentage, + totalInvestmentDays + }; + } + const newGrossPerformance = valueOfInvestment.Value.minus( totalInvestment.Value ).plus(grossPerformanceFromSells.Value); @@ -1829,6 +1922,7 @@ export class PortfolioCalculator { }; } + @LogPerformance private handleFeeAndUnitPriceOfOrder( order: PortfolioOrderItem, currentExchangeRate: number, @@ -1852,6 +1946,7 @@ export class PortfolioCalculator { } } + @LogPerformance private calculateNetPerformancePercentageForDateAndSymbol( i: number, orders: PortfolioOrderItem[], @@ -1895,6 +1990,7 @@ export class PortfolioCalculator { } } + @LogPerformance private handleIfPreviousOrderIsStake( netPerformanceValuesPercentage: { [date: string]: Big }, order: PortfolioOrderItem, @@ -1906,6 +2002,7 @@ export class PortfolioCalculator { .minus(1); } + @LogPerformance private stakeHandling( previousOrder: PortfolioOrderItem, marketSymbolMap: { [date: string]: { [symbol: string]: Big } }, @@ -1925,6 +2022,7 @@ export class PortfolioCalculator { : new Big(0); } + @LogPerformance private ispreviousOrderStakeAndHasInformation( previousOrder: PortfolioOrderItem, marketSymbolMap: { [date: string]: { [symbol: string]: Big } } @@ -1936,6 +2034,7 @@ export class PortfolioCalculator { ); } + @LogPerformance private needsStakeHandling( order: PortfolioOrderItem, marketSymbolMap: { [date: string]: { [symbol: string]: Big } }, @@ -1952,6 +2051,7 @@ export class PortfolioCalculator { ); } + @LogPerformance private handleLoggingOfInvestmentMetrics( totalInvestment: WithCurrencyEffect, order: PortfolioOrderItem, @@ -1986,6 +2086,7 @@ export class PortfolioCalculator { } } + @LogPerformance private calculateNetPerformancePercentage( timeWeightedAverageInvestmentBetweenStartAndEndDate: WithCurrencyEffect, totalNetPerformance: WithCurrencyEffect @@ -2007,6 +2108,7 @@ export class PortfolioCalculator { }; } + @LogPerformance private calculateInvestmentSpecificMetrics( averagePriceAtStartDate: Big, i: number, @@ -2132,6 +2234,7 @@ export class PortfolioCalculator { }; } + @LogPerformance private calculatePerformancesForDateAndReturnTotalInvestmentDays( isChartMode: boolean, i: number, @@ -2234,6 +2337,7 @@ export class PortfolioCalculator { return totalInvestmentDays; } + @LogPerformance private calculateSellOrders( order: PortfolioOrderItem, lastAveragePrice: WithCurrencyEffect, @@ -2280,6 +2384,7 @@ export class PortfolioCalculator { }; } + @LogPerformance private calculateInitialValue( i: number, indexOfStartOrder: number, @@ -2315,6 +2420,7 @@ export class PortfolioCalculator { }; } + @LogPerformance private calculateAveragePriceAtEnd( i: number, indexOfEndOrder: number, @@ -2328,6 +2434,7 @@ export class PortfolioCalculator { return averagePriceAtEndDate; } + @LogPerformance private getTransactionInvestment( order: PortfolioOrderItem, totalUnits: Big, @@ -2350,6 +2457,7 @@ export class PortfolioCalculator { : new Big(0); } + @LogPerformance private calculateAveragePrice( averagePriceAtStartDate: Big, i: number, @@ -2367,6 +2475,7 @@ export class PortfolioCalculator { return averagePriceAtStartDate; } + @LogPerformance private handleStartOrder( order: PortfolioOrderItem, indexOfStartOrder: number, @@ -2384,6 +2493,7 @@ export class PortfolioCalculator { } } + @LogPerformance private handleLogging( symbol: string, orders: PortfolioOrderItem[], @@ -2429,6 +2539,7 @@ export class PortfolioCalculator { } } + @LogPerformance private sortOrdersByTime(orders: PortfolioOrderItem[]) { return sortBy(orders, (order) => { let sortIndex = new Date(order.date); @@ -2445,6 +2556,7 @@ export class PortfolioCalculator { }); } + @LogPerformance private handleChartMode( isChartMode: boolean, orders: PortfolioOrderItem[], @@ -2480,6 +2592,7 @@ export class PortfolioCalculator { return { day, lastUnitPrice }; } + @LogPerformance private handleDay( datesWithOrders: {}, day: Date, @@ -2519,6 +2632,7 @@ export class PortfolioCalculator { } } + @LogPerformance private addSyntheticStartAndEndOrders( orders: PortfolioOrderItem[], symbol: string, diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 59d7c2f03..ec33e7b27 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -1,3 +1,4 @@ +import { LogPerformance } from '@ghostfolio/api/aop/logging.interceptor'; import { AccessService } from '@ghostfolio/api/app/access/access.service'; import { UserService } from '@ghostfolio/api/app/user/user.service'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; @@ -71,7 +72,8 @@ export class PortfolioController { @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('range') dateRange: DateRange = 'max', - @Query('tags') filterByTags?: string + @Query('tags') filterByTags?: string, + @Query('isAllocation') isAllocation: boolean = false ): Promise { let hasDetails = true; let hasError = false; @@ -104,7 +106,8 @@ export class PortfolioController { dateRange, filters, impersonationId, - userId: this.request.user.id + userId: this.request.user.id, + isAllocation }); if (hasErrors || hasNotDefinedValuesInObject(holdings)) { @@ -375,6 +378,7 @@ export class PortfolioController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInResponseInterceptor) @Version('2') + @LogPerformance public async getPerformanceV2( @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Query('accounts') filterByAccounts?: string, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 7d39d8d17..44bc16b83 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1,3 +1,4 @@ +import { LogPerformance } from '@ghostfolio/api/aop/logging.interceptor'; import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface'; @@ -113,6 +114,7 @@ export class PortfolioService { private readonly userService: UserService ) {} + @LogPerformance public async getAccounts({ filters, userId, @@ -182,6 +184,7 @@ export class PortfolioService { }); } + @LogPerformance public async getAccountsWithAggregations({ filters, userId, @@ -218,6 +221,7 @@ export class PortfolioService { }; } + @LogPerformance public async getDividends({ dateRange, filters, @@ -259,6 +263,7 @@ export class PortfolioService { }); } + @LogPerformance public async getInvestments({ dateRange, filters, @@ -332,18 +337,21 @@ export class PortfolioService { }; } + @LogPerformance public async getDetails({ dateRange = 'max', filters, impersonationId, userId, - withExcludedAccounts = false + withExcludedAccounts = false, + isAllocation = false }: { dateRange?: DateRange; filters?: Filter[]; impersonationId: string; userId: string; withExcludedAccounts?: boolean; + isAllocation?: boolean; }): Promise { userId = await this.getUserId(impersonationId, userId); const user = await this.userService.user({ id: userId }); @@ -373,8 +381,11 @@ export class PortfolioService { transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT) ); const startDate = this.getStartDate(dateRange, portfolioStart); - const currentPositions = - await portfolioCalculator.getCurrentPositions(startDate); + const currentPositions = await portfolioCalculator.getCurrentPositions( + startDate, + new Date(Date.now()), + !isAllocation + ); const cashDetails = await this.accountService.getCashDetails({ filters, @@ -465,17 +476,19 @@ export class PortfolioService { accounts, holdings ); - - const summary = await this.getSummary({ - impersonationId, - userCurrency, - userId, - balanceInBaseCurrency: cashDetails.balanceInBaseCurrency, - emergencyFundPositionsValueInBaseCurrency: - this.getEmergencyFundPositionsValueInBaseCurrency({ - holdings - }) - }); + let summary; + if (!isAllocation) { + summary = await this.getSummary({ + impersonationId, + userCurrency, + userId, + balanceInBaseCurrency: cashDetails.balanceInBaseCurrency, + emergencyFundPositionsValueInBaseCurrency: + this.getEmergencyFundPositionsValueInBaseCurrency({ + holdings + }) + }); + } return { accounts, @@ -483,14 +496,20 @@ export class PortfolioService { platforms, summary, filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), - filteredValueInPercentage: summary.netWorth + filteredValueInPercentage: summary?.netWorth ? filteredValueInBaseCurrency.div(summary.netWorth).toNumber() : 0, hasErrors: currentPositions.hasErrors, - totalValueInBaseCurrency: summary.netWorth + totalValueInBaseCurrency: + summary?.netWorth ?? + Object.keys(holdings).reduce( + (s, k) => s + holdings[k].valueInBaseCurrency ?? 0, + 0 + ) }; } + @LogPerformance private handlePositions( currentPositions: CurrentPositions, portfolioItemsNow: { [symbol: string]: TimelinePosition }, @@ -567,6 +586,7 @@ export class PortfolioService { } } + @LogPerformance private async handleCashPosition( filters: Filter[], isFilteredByAccount: boolean, @@ -592,6 +612,7 @@ export class PortfolioService { } } + @LogPerformance private async handleEmergencyFunds( filters: Filter[], cashDetails: CashDetails, @@ -647,6 +668,7 @@ export class PortfolioService { return filteredValueInBaseCurrency; } + @LogPerformance private calculateMarketsAllocation( symbolProfile: EnhancedSymbolProfile, markets: { @@ -719,6 +741,7 @@ export class PortfolioService { } } + @LogPerformance public async getPosition( aDataSource: DataSource, aImpersonationId: string, @@ -1049,6 +1072,7 @@ export class PortfolioService { } } + @LogPerformance public async getPositions({ dateRange = 'max', filters, @@ -1192,6 +1216,7 @@ export class PortfolioService { }; } + @LogPerformance public async getPerformance({ dateRange = 'max', filters, @@ -1386,6 +1411,7 @@ export class PortfolioService { }; } + @LogPerformance public async getReport(impersonationId: string): Promise { const userId = await this.getUserId(impersonationId, this.request.user.id); const user = await this.userService.user({ id: userId }); @@ -1485,6 +1511,7 @@ export class PortfolioService { }; } + @LogPerformance private async getCashPositions({ cashDetails, userCurrency, @@ -1535,6 +1562,7 @@ export class PortfolioService { return cashPositions; } + @LogPerformance private async getChart({ dateRange = 'max', impersonationId, @@ -1599,6 +1627,7 @@ export class PortfolioService { }; } + @LogPerformance private getDividendsByGroup({ dividends, groupBy @@ -1661,6 +1690,7 @@ export class PortfolioService { return dividendsByGroup; } + @LogPerformance private getEmergencyFundPositionsValueInBaseCurrency({ holdings }: { @@ -1684,6 +1714,7 @@ export class PortfolioService { return valueInBaseCurrencyOfEmergencyFundPositions.toNumber(); } + @LogPerformance private getFees({ activities, date = new Date(0), @@ -1711,6 +1742,7 @@ export class PortfolioService { ); } + @LogPerformance private getInitialCashPosition({ balance, currency @@ -1743,6 +1775,7 @@ export class PortfolioService { }; } + @LogPerformance private getStartDate(aDateRange: DateRange, portfolioStart: Date) { switch (aDateRange) { case '1d': @@ -1811,6 +1844,7 @@ export class PortfolioService { return portfolioStart; } + @LogPerformance private getStreaks({ investments, savingsRate @@ -1833,6 +1867,7 @@ export class PortfolioService { return { currentStreak, longestStreak }; } + @LogPerformance private async getSummary({ balanceInBaseCurrency, emergencyFundPositionsValueInBaseCurrency, @@ -2003,6 +2038,7 @@ export class PortfolioService { }; } + @LogPerformance private getSumOfActivityType({ activities, activityType, @@ -2036,6 +2072,7 @@ export class PortfolioService { ); } + @LogPerformance private async getTransactionPoints({ filters, includeDrafts = false, @@ -2111,6 +2148,7 @@ export class PortfolioService { return impersonationUserId || aUserId; } + @LogPerformance private async getValueOfAccountsAndPlatforms({ filters = [], orders, @@ -2261,6 +2299,7 @@ export class PortfolioService { return { accounts, platforms }; } + @LogPerformance private mergeHistoricalDataItems( accountBalanceItems: HistoricalDataItem[], performanceChartItems: HistoricalDataItem[] diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index 330ae4227..44a95d48f 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -205,7 +205,10 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { private fetchPortfolioDetails() { return this.dataService.fetchPortfolioDetails({ - filters: this.userService.getFilters() + filters: this.userService.getFilters(), + parameters: { + isAllocation: true + } }); } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 89ff0c3f3..c55d4e818 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -389,13 +389,25 @@ export class DataService { } public fetchPortfolioDetails({ - filters + filters, + parameters }: { filters?: Filter[]; + parameters?: { + [param: string]: + | string + | number + | boolean + | readonly (string | number | boolean)[]; + }; } = {}): Observable { + let params = this.buildFiltersAsQueryParams({ filters }).appendAll( + parameters + ); + return this.http .get('/api/v1/portfolio/details', { - params: this.buildFiltersAsQueryParams({ filters }) + params }) .pipe( map((response) => { From ea39ab15ea94cbc8e422718ee5e866bae5d1ede2 Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 2 Mar 2024 13:49:01 +0100 Subject: [PATCH 3/4] Fix Asset Allocation Overview --- .../src/app/portfolio/portfolio.service.ts | 88 +++++++++++++++---- 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 44bc16b83..5023cc99b 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -490,22 +490,21 @@ export class PortfolioService { }); } + var netWorth = + summary?.netWorth ?? + (await this.getNetWorth(impersonationId, userId, userCurrency)); + return { accounts, holdings, platforms, summary, filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), - filteredValueInPercentage: summary?.netWorth - ? filteredValueInBaseCurrency.div(summary.netWorth).toNumber() + filteredValueInPercentage: netWorth + ? filteredValueInBaseCurrency.div(netWorth).toNumber() : 0, hasErrors: currentPositions.hasErrors, - totalValueInBaseCurrency: - summary?.netWorth ?? - Object.keys(holdings).reduce( - (s, k) => s + holdings[k].valueInBaseCurrency ?? 0, - 0 - ) + totalValueInBaseCurrency: netWorth }; } @@ -1867,6 +1866,42 @@ export class PortfolioService { return { currentStreak, longestStreak }; } + @LogPerformance + private async getNetWorth( + impersonationId: string, + userId: string, + userCurrency: string + ) { + userId = await this.getUserId(impersonationId, userId); + + const { orders, portfolioOrders, transactionPoints } = + await this.getTransactionPoints({ + userId, + withExcludedAccounts: true + }); + + const portfolioCalculator = new PortfolioCalculator({ + currency: userCurrency, + currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService, + orders: portfolioOrders + }); + + const portfolioStart = parseDate( + transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT) + ); + + portfolioCalculator.setTransactionPoints(transactionPoints); + + const { currentValue } = await portfolioCalculator.getCurrentPositions( + portfolioStart, + new Date(Date.now()), + false + ); + + return currentValue; + } + @LogPerformance private async getSummary({ balanceInBaseCurrency, @@ -1883,11 +1918,26 @@ export class PortfolioService { }): Promise { userId = await this.getUserId(impersonationId, userId); const user = await this.userService.user({ id: userId }); - - const performanceInformation = await this.getPerformance({ - impersonationId, - userId - }); + let performanceInformation: PortfolioPerformanceResponse = { + chart: [], + firstOrderDate: undefined, + performance: { + annualizedPerformancePercent: 0, + currentGrossPerformance: 0, + currentGrossPerformancePercent: 0, + currentGrossPerformancePercentWithCurrencyEffect: 0, + currentGrossPerformanceWithCurrencyEffect: 0, + currentNetPerformance: 0, + currentNetPerformancePercent: 0, + currentNetPerformancePercentWithCurrencyEffect: 0, + currentNetPerformanceWithCurrencyEffect: 0, + currentNetWorth: 0, + currentValue: 0, + totalInvestment: 0 + }, + errors: [], + hasErrors: false + }; const { activities } = await this.orderService.getOrders({ userCurrency, @@ -1906,6 +1956,13 @@ export class PortfolioService { let totalSell = 0; let activitiesUsed: Activity[] = []; let ordersCount = 0; + let excludedAccountsAndActivities = 0; + const firstOrderDate = activities[0]?.date; + + performanceInformation = await this.getPerformance({ + impersonationId, + userId + }); for (let order of activities) { if (order.Account?.isExcluded ?? false) { excludedActivities.push(order); @@ -1952,8 +2009,6 @@ export class PortfolioService { ) ); - const firstOrderDate = activitiesUsed[0]?.date; - const cash = new Big(balanceInBaseCurrency) .minus(emergencyFund) .plus(emergencyFundPositionsValueInBaseCurrency) @@ -1977,12 +2032,11 @@ export class PortfolioService { currency: userCurrency, withExcludedAccounts: true }); - const excludedBalanceInBaseCurrency = new Big( cashDetailsWithExcludedAccounts.balanceInBaseCurrency ).minus(balanceInBaseCurrency); - const excludedAccountsAndActivities = excludedBalanceInBaseCurrency + excludedAccountsAndActivities = excludedBalanceInBaseCurrency .plus(totalOfExcludedActivities) .toNumber(); From f0e0da09a27d067d44fc3866b1d8ff60e1085344 Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 2 Mar 2024 14:01:49 +0100 Subject: [PATCH 4/4] Added Log Level to env --- apps/api/src/main.ts | 30 +++++++++++++++++-- .../configuration/configuration.service.ts | 1 + 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index b7b9a548a..31598f249 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -14,10 +14,34 @@ async function bootstrap() { const configApp = await NestFactory.create(AppModule); const configService = configApp.get(ConfigService); + let logLevelArray = []; + let logLevel = configService.get('LOG_LEVEL'); + + switch (logLevel) { + case 'verbose': + logLevelArray.push(['debug', 'error', 'log', 'verbose', 'warn']); + break; + case 'debug': + logLevelArray.push(['debug', 'error', 'log', 'warn']); + break; + case 'log': + logLevelArray.push([, 'error', 'log', 'warn']); + break; + case 'warn': + logLevelArray.push(['error', 'warn']); + break; + case 'error': + logLevelArray.push(['error']); + break; + default: + logLevelArray = environment.production + ? ['error', 'log', 'warn'] + : ['debug', 'error', 'log', 'verbose', 'warn']; + break; + } + const app = await NestFactory.create(AppModule, { - logger: environment.production - ? ['error', 'log', 'warn'] - : ['debug', 'error', 'log', 'verbose', 'warn'] + logger: logLevelArray }); app.enableCors(); diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index 61a54daa1..6d2daee55 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -22,6 +22,7 @@ export class ConfigurationService { API_KEY_RAPID_API: str({ default: '' }), CACHE_QUOTES_TTL: num({ default: 1 }), CACHE_TTL: num({ default: 1 }), + LOG_LEVEL: str({ default: '' }), DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }), DATA_SOURCE_IMPORT: str({ default: DataSource.YAHOO }), DATA_SOURCES: json({