Browse Source

Merge pull request #69 from dandevaud/feature/Performance-Enhancements

Feature/performance enhancements
pull/5027/head
dandevaud 1 year ago
committed by GitHub
parent
commit
aa3a3ef42c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 55
      apps/api/src/aop/logging.interceptor.ts
  2. 2
      apps/api/src/app/account-balance/account-balance.service.ts
  3. 162
      apps/api/src/app/portfolio/portfolio-calculator.ts
  4. 8
      apps/api/src/app/portfolio/portfolio.controller.ts
  5. 143
      apps/api/src/app/portfolio/portfolio.service.ts
  6. 32
      apps/api/src/main.ts
  7. 1
      apps/api/src/services/configuration/configuration.service.ts
  8. 2
      apps/api/src/services/data-provider/data-provider.service.ts
  9. 2
      apps/api/src/services/symbol-profile/symbol-profile.service.ts
  10. 5
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  11. 16
      apps/client/src/app/services/data.service.ts

55
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<any> {
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;
}

2
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces'; import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces';
@ -40,6 +41,7 @@ export class AccountBalanceService {
}); });
} }
@LogPerformance
public async getAccountBalances({ public async getAccountBalances({
filters, filters,
user, user,

162
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 { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
@ -80,6 +81,7 @@ export class PortfolioCalculator {
}); });
} }
@LogPerformance
public computeTransactionPoints() { public computeTransactionPoints() {
this.transactionPoints = []; this.transactionPoints = [];
const symbols: { [symbol: string]: TransactionPointSymbol } = {}; const symbols: { [symbol: string]: TransactionPointSymbol } = {};
@ -123,6 +125,7 @@ export class PortfolioCalculator {
} }
} }
@LogPerformance
private getCurrentTransactionPointItem( private getCurrentTransactionPointItem(
oldAccumulatedSymbol: TransactionPointSymbol, oldAccumulatedSymbol: TransactionPointSymbol,
order: PortfolioOrder, order: PortfolioOrder,
@ -154,6 +157,7 @@ export class PortfolioCalculator {
return currentTransactionPointItem; return currentTransactionPointItem;
} }
@LogPerformance
private handleSubsequentTransactions( private handleSubsequentTransactions(
order: PortfolioOrder, order: PortfolioOrder,
factor: number, factor: number,
@ -196,6 +200,7 @@ export class PortfolioCalculator {
return currentTransactionPointItem; return currentTransactionPointItem;
} }
@LogPerformance
public getAnnualizedPerformancePercent({ public getAnnualizedPerformancePercent({
daysInMarket, daysInMarket,
netPerformancePercent netPerformancePercent
@ -221,6 +226,7 @@ export class PortfolioCalculator {
this.transactionPoints = transactionPoints; this.transactionPoints = transactionPoints;
} }
@LogPerformance
public async getChartData({ public async getChartData({
start, start,
end = new Date(Date.now()), end = new Date(Date.now()),
@ -345,6 +351,7 @@ export class PortfolioCalculator {
}); });
} }
@LogPerformance
private calculatePerformance( private calculatePerformance(
date: Date, date: Date,
previousDate: Date, previousDate: Date,
@ -479,6 +486,7 @@ export class PortfolioCalculator {
}; };
} }
@LogPerformance
private accumulatedValuesByDate( private accumulatedValuesByDate(
valuesBySymbol: { valuesBySymbol: {
[symbol: string]: { [symbol: string]: {
@ -587,6 +595,7 @@ export class PortfolioCalculator {
return accumulatedValuesByDate; return accumulatedValuesByDate;
} }
@LogPerformance
private populateSymbolMetrics( private populateSymbolMetrics(
symbols: { [symbol: string]: boolean }, symbols: { [symbol: string]: boolean },
end: Date, end: Date,
@ -648,6 +657,7 @@ export class PortfolioCalculator {
} }
} }
@LogPerformance
private populateMarketSymbolMap( private populateMarketSymbolMap(
marketSymbols: GetValueObject[], marketSymbols: GetValueObject[],
marketSymbolMap: { [date: string]: { [symbol: string]: Big } } marketSymbolMap: { [date: string]: { [symbol: string]: Big } }
@ -665,6 +675,7 @@ export class PortfolioCalculator {
} }
} }
@LogPerformance
private async getInformationFromCurrentRateService( private async getInformationFromCurrentRateService(
currencies: { [symbol: string]: string }, currencies: { [symbol: string]: string },
dataGatheringItems: IDataGatheringItem[], dataGatheringItems: IDataGatheringItem[],
@ -681,6 +692,7 @@ export class PortfolioCalculator {
}); });
} }
@LogPerformance
private pushDataGatheringsSymbols( private pushDataGatheringsSymbols(
transactionPointsBeforeEndDate: TransactionPoint[], transactionPointsBeforeEndDate: TransactionPoint[],
firstIndex: number, firstIndex: number,
@ -700,6 +712,7 @@ export class PortfolioCalculator {
} }
} }
@LogPerformance
private getRelevantStartAndEndDates( private getRelevantStartAndEndDates(
start: Date, start: Date,
end: Date, end: Date,
@ -718,9 +731,11 @@ export class PortfolioCalculator {
} }
} }
@LogPerformance
public async getCurrentPositions( public async getCurrentPositions(
start: Date, start: Date,
end = new Date(Date.now()) end = new Date(Date.now()),
calculatePerformance = true
): Promise<CurrentPositions> { ): Promise<CurrentPositions> {
const transactionPointsBeforeEndDate = const transactionPointsBeforeEndDate =
this.transactionPoints?.filter((transactionPoint) => { this.transactionPoints?.filter((transactionPoint) => {
@ -883,7 +898,8 @@ export class PortfolioCalculator {
start, start,
exchangeRates: exchangeRates:
exchangeRatesByCurrency[`${item.currency}${this.currency}`], exchangeRatesByCurrency[`${item.currency}${this.currency}`],
symbol: item.symbol symbol: item.symbol,
calculatePerformance
}); });
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors; hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
@ -956,6 +972,7 @@ export class PortfolioCalculator {
return this.dataProviderInfos; return this.dataProviderInfos;
} }
@LogPerformance
public getInvestments(): { date: string; investment: Big }[] { public getInvestments(): { date: string; investment: Big }[] {
if (this.transactionPoints.length === 0) { if (this.transactionPoints.length === 0) {
return []; return [];
@ -973,6 +990,7 @@ export class PortfolioCalculator {
}); });
} }
@LogPerformance
public getInvestmentsByGroup({ public getInvestmentsByGroup({
data, data,
groupBy groupBy
@ -996,6 +1014,7 @@ export class PortfolioCalculator {
})); }));
} }
@LogPerformance
private calculateOverallPerformance(positions: TimelinePosition[]) { private calculateOverallPerformance(positions: TimelinePosition[]) {
let currentValue = new Big(0); let currentValue = new Big(0);
let grossPerformance = new Big(0); let grossPerformance = new Big(0);
@ -1121,6 +1140,7 @@ export class PortfolioCalculator {
return factor; return factor;
} }
@LogPerformance
private getSymbolMetrics({ private getSymbolMetrics({
end, end,
exchangeRates, exchangeRates,
@ -1128,7 +1148,8 @@ export class PortfolioCalculator {
marketSymbolMap, marketSymbolMap,
start, start,
step = 1, step = 1,
symbol symbol,
calculatePerformance = true
}: { }: {
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
@ -1139,6 +1160,7 @@ export class PortfolioCalculator {
start: Date; start: Date;
step?: number; step?: number;
symbol: string; symbol: string;
calculatePerformance?: boolean;
}): SymbolMetrics { }): SymbolMetrics {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const currentValues: WithCurrencyEffect<{ [date: string]: Big }> = { const currentValues: WithCurrencyEffect<{ [date: string]: Big }> = {
@ -1359,7 +1381,8 @@ export class PortfolioCalculator {
unitPriceAtEndDate, unitPriceAtEndDate,
symbol, symbol,
exchangeRates, exchangeRates,
currentExchangeRate currentExchangeRate,
calculatePerformance
); );
return { return {
@ -1403,6 +1426,7 @@ export class PortfolioCalculator {
}; };
} }
@LogPerformance
private calculatePerformanceOfSymbol( private calculatePerformanceOfSymbol(
orders: PortfolioOrderItem[], orders: PortfolioOrderItem[],
indexOfStartOrder: number, indexOfStartOrder: number,
@ -1435,7 +1459,8 @@ export class PortfolioCalculator {
unitPriceAtEndDate: Big, unitPriceAtEndDate: Big,
symbol: string, symbol: string,
exchangeRates: { [dateString: string]: number }, exchangeRates: { [dateString: string]: number },
currentExchangeRate: number currentExchangeRate: number,
calculatePerformance: boolean
) { ) {
let totalInvestmentDays = 0; let totalInvestmentDays = 0;
let sumOfTimeWeightedInvestments = { let sumOfTimeWeightedInvestments = {
@ -1490,9 +1515,51 @@ export class PortfolioCalculator {
sumOfTimeWeightedInvestments, sumOfTimeWeightedInvestments,
timeWeightedInvestmentValues, timeWeightedInvestmentValues,
exchangeRates, 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 = { const totalGrossPerformance = {
Value: grossPerformance.Value.minus(grossPerformanceAtStartDate.Value), Value: grossPerformance.Value.minus(grossPerformanceAtStartDate.Value),
WithCurrencyEffect: grossPerformance.WithCurrencyEffect.minus( WithCurrencyEffect: grossPerformance.WithCurrencyEffect.minus(
@ -1601,6 +1668,7 @@ export class PortfolioCalculator {
}; };
} }
@LogPerformance
private handleOrders( private handleOrders(
orders: PortfolioOrderItem[], orders: PortfolioOrderItem[],
indexOfStartOrder: number, indexOfStartOrder: number,
@ -1632,18 +1700,21 @@ export class PortfolioCalculator {
sumOfTimeWeightedInvestments: WithCurrencyEffect<Big>, sumOfTimeWeightedInvestments: WithCurrencyEffect<Big>,
timeWeightedInvestmentValues: WithCurrencyEffect<{ [date: string]: Big }>, timeWeightedInvestmentValues: WithCurrencyEffect<{ [date: string]: Big }>,
exchangeRates: { [dateString: string]: number }, exchangeRates: { [dateString: string]: number },
currentExchangeRate: number currentExchangeRate: number,
calculatePerformance: boolean
) { ) {
for (let i = 0; i < orders.length; i += 1) { for (let i = 0; i < orders.length; i += 1) {
const order = orders[i]; const order = orders[i];
const previousOrderDateString = i > 0 ? orders[i - 1].date : ''; const previousOrderDateString = i > 0 ? orders[i - 1].date : '';
this.calculateNetPerformancePercentageForDateAndSymbol( if (calculatePerformance) {
i, this.calculateNetPerformancePercentageForDateAndSymbol(
orders, i,
order, orders,
netPerformanceValuesPercentage, order,
marketSymbolMap netPerformanceValuesPercentage,
); marketSymbolMap
);
}
if (PortfolioCalculator.ENABLE_LOGGING) { if (PortfolioCalculator.ENABLE_LOGGING) {
console.log(); console.log();
@ -1704,16 +1775,18 @@ export class PortfolioCalculator {
fees fees
)); ));
({ if (calculatePerformance) {
grossPerformanceFromSells, ({
totalInvestmentWithGrossPerformanceFromSell grossPerformanceFromSells,
} = this.calculateSellOrders( totalInvestmentWithGrossPerformanceFromSell
order, } = this.calculateSellOrders(
lastAveragePrice, order,
grossPerformanceFromSells, lastAveragePrice,
totalInvestmentWithGrossPerformanceFromSell, grossPerformanceFromSells,
transactionInvestment totalInvestmentWithGrossPerformanceFromSell,
)); transactionInvestment
));
}
lastAveragePrice.Value = totalUnits.eq(0) lastAveragePrice.Value = totalUnits.eq(0)
? new Big(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( const newGrossPerformance = valueOfInvestment.Value.minus(
totalInvestment.Value totalInvestment.Value
).plus(grossPerformanceFromSells.Value); ).plus(grossPerformanceFromSells.Value);
@ -1829,6 +1922,7 @@ export class PortfolioCalculator {
}; };
} }
@LogPerformance
private handleFeeAndUnitPriceOfOrder( private handleFeeAndUnitPriceOfOrder(
order: PortfolioOrderItem, order: PortfolioOrderItem,
currentExchangeRate: number, currentExchangeRate: number,
@ -1852,6 +1946,7 @@ export class PortfolioCalculator {
} }
} }
@LogPerformance
private calculateNetPerformancePercentageForDateAndSymbol( private calculateNetPerformancePercentageForDateAndSymbol(
i: number, i: number,
orders: PortfolioOrderItem[], orders: PortfolioOrderItem[],
@ -1895,6 +1990,7 @@ export class PortfolioCalculator {
} }
} }
@LogPerformance
private handleIfPreviousOrderIsStake( private handleIfPreviousOrderIsStake(
netPerformanceValuesPercentage: { [date: string]: Big }, netPerformanceValuesPercentage: { [date: string]: Big },
order: PortfolioOrderItem, order: PortfolioOrderItem,
@ -1906,6 +2002,7 @@ export class PortfolioCalculator {
.minus(1); .minus(1);
} }
@LogPerformance
private stakeHandling( private stakeHandling(
previousOrder: PortfolioOrderItem, previousOrder: PortfolioOrderItem,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } }, marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
@ -1925,6 +2022,7 @@ export class PortfolioCalculator {
: new Big(0); : new Big(0);
} }
@LogPerformance
private ispreviousOrderStakeAndHasInformation( private ispreviousOrderStakeAndHasInformation(
previousOrder: PortfolioOrderItem, previousOrder: PortfolioOrderItem,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } } marketSymbolMap: { [date: string]: { [symbol: string]: Big } }
@ -1936,6 +2034,7 @@ export class PortfolioCalculator {
); );
} }
@LogPerformance
private needsStakeHandling( private needsStakeHandling(
order: PortfolioOrderItem, order: PortfolioOrderItem,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } }, marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
@ -1952,6 +2051,7 @@ export class PortfolioCalculator {
); );
} }
@LogPerformance
private handleLoggingOfInvestmentMetrics( private handleLoggingOfInvestmentMetrics(
totalInvestment: WithCurrencyEffect<Big>, totalInvestment: WithCurrencyEffect<Big>,
order: PortfolioOrderItem, order: PortfolioOrderItem,
@ -1986,6 +2086,7 @@ export class PortfolioCalculator {
} }
} }
@LogPerformance
private calculateNetPerformancePercentage( private calculateNetPerformancePercentage(
timeWeightedAverageInvestmentBetweenStartAndEndDate: WithCurrencyEffect<Big>, timeWeightedAverageInvestmentBetweenStartAndEndDate: WithCurrencyEffect<Big>,
totalNetPerformance: WithCurrencyEffect<Big> totalNetPerformance: WithCurrencyEffect<Big>
@ -2007,6 +2108,7 @@ export class PortfolioCalculator {
}; };
} }
@LogPerformance
private calculateInvestmentSpecificMetrics( private calculateInvestmentSpecificMetrics(
averagePriceAtStartDate: Big, averagePriceAtStartDate: Big,
i: number, i: number,
@ -2132,6 +2234,7 @@ export class PortfolioCalculator {
}; };
} }
@LogPerformance
private calculatePerformancesForDateAndReturnTotalInvestmentDays( private calculatePerformancesForDateAndReturnTotalInvestmentDays(
isChartMode: boolean, isChartMode: boolean,
i: number, i: number,
@ -2234,6 +2337,7 @@ export class PortfolioCalculator {
return totalInvestmentDays; return totalInvestmentDays;
} }
@LogPerformance
private calculateSellOrders( private calculateSellOrders(
order: PortfolioOrderItem, order: PortfolioOrderItem,
lastAveragePrice: WithCurrencyEffect<Big>, lastAveragePrice: WithCurrencyEffect<Big>,
@ -2280,6 +2384,7 @@ export class PortfolioCalculator {
}; };
} }
@LogPerformance
private calculateInitialValue( private calculateInitialValue(
i: number, i: number,
indexOfStartOrder: number, indexOfStartOrder: number,
@ -2315,6 +2420,7 @@ export class PortfolioCalculator {
}; };
} }
@LogPerformance
private calculateAveragePriceAtEnd( private calculateAveragePriceAtEnd(
i: number, i: number,
indexOfEndOrder: number, indexOfEndOrder: number,
@ -2328,6 +2434,7 @@ export class PortfolioCalculator {
return averagePriceAtEndDate; return averagePriceAtEndDate;
} }
@LogPerformance
private getTransactionInvestment( private getTransactionInvestment(
order: PortfolioOrderItem, order: PortfolioOrderItem,
totalUnits: Big, totalUnits: Big,
@ -2350,6 +2457,7 @@ export class PortfolioCalculator {
: new Big(0); : new Big(0);
} }
@LogPerformance
private calculateAveragePrice( private calculateAveragePrice(
averagePriceAtStartDate: Big, averagePriceAtStartDate: Big,
i: number, i: number,
@ -2367,6 +2475,7 @@ export class PortfolioCalculator {
return averagePriceAtStartDate; return averagePriceAtStartDate;
} }
@LogPerformance
private handleStartOrder( private handleStartOrder(
order: PortfolioOrderItem, order: PortfolioOrderItem,
indexOfStartOrder: number, indexOfStartOrder: number,
@ -2384,6 +2493,7 @@ export class PortfolioCalculator {
} }
} }
@LogPerformance
private handleLogging( private handleLogging(
symbol: string, symbol: string,
orders: PortfolioOrderItem[], orders: PortfolioOrderItem[],
@ -2429,6 +2539,7 @@ export class PortfolioCalculator {
} }
} }
@LogPerformance
private sortOrdersByTime(orders: PortfolioOrderItem[]) { private sortOrdersByTime(orders: PortfolioOrderItem[]) {
return sortBy(orders, (order) => { return sortBy(orders, (order) => {
let sortIndex = new Date(order.date); let sortIndex = new Date(order.date);
@ -2445,6 +2556,7 @@ export class PortfolioCalculator {
}); });
} }
@LogPerformance
private handleChartMode( private handleChartMode(
isChartMode: boolean, isChartMode: boolean,
orders: PortfolioOrderItem[], orders: PortfolioOrderItem[],
@ -2480,6 +2592,7 @@ export class PortfolioCalculator {
return { day, lastUnitPrice }; return { day, lastUnitPrice };
} }
@LogPerformance
private handleDay( private handleDay(
datesWithOrders: {}, datesWithOrders: {},
day: Date, day: Date,
@ -2519,6 +2632,7 @@ export class PortfolioCalculator {
} }
} }
@LogPerformance
private addSyntheticStartAndEndOrders( private addSyntheticStartAndEndOrders(
orders: PortfolioOrderItem[], orders: PortfolioOrderItem[],
symbol: string, symbol: string,

8
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 { AccessService } from '@ghostfolio/api/app/access/access.service';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
@ -71,7 +72,8 @@ export class PortfolioController {
@Query('accounts') filterByAccounts?: string, @Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string, @Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max', @Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string @Query('tags') filterByTags?: string,
@Query('isAllocation') isAllocation: boolean = false
): Promise<PortfolioDetails & { hasError: boolean }> { ): Promise<PortfolioDetails & { hasError: boolean }> {
let hasDetails = true; let hasDetails = true;
let hasError = false; let hasError = false;
@ -104,7 +106,8 @@ export class PortfolioController {
dateRange, dateRange,
filters, filters,
impersonationId, impersonationId,
userId: this.request.user.id userId: this.request.user.id,
isAllocation
}); });
if (hasErrors || hasNotDefinedValuesInObject(holdings)) { if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
@ -375,6 +378,7 @@ export class PortfolioController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
@Version('2') @Version('2')
@LogPerformance
public async getPerformanceV2( public async getPerformanceV2(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string, @Query('accounts') filterByAccounts?: string,

143
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 { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface'; import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
@ -113,6 +114,7 @@ export class PortfolioService {
private readonly userService: UserService private readonly userService: UserService
) {} ) {}
@LogPerformance
public async getAccounts({ public async getAccounts({
filters, filters,
userId, userId,
@ -182,6 +184,7 @@ export class PortfolioService {
}); });
} }
@LogPerformance
public async getAccountsWithAggregations({ public async getAccountsWithAggregations({
filters, filters,
userId, userId,
@ -218,6 +221,7 @@ export class PortfolioService {
}; };
} }
@LogPerformance
public async getDividends({ public async getDividends({
dateRange, dateRange,
filters, filters,
@ -259,6 +263,7 @@ export class PortfolioService {
}); });
} }
@LogPerformance
public async getInvestments({ public async getInvestments({
dateRange, dateRange,
filters, filters,
@ -332,18 +337,21 @@ export class PortfolioService {
}; };
} }
@LogPerformance
public async getDetails({ public async getDetails({
dateRange = 'max', dateRange = 'max',
filters, filters,
impersonationId, impersonationId,
userId, userId,
withExcludedAccounts = false withExcludedAccounts = false,
isAllocation = false
}: { }: {
dateRange?: DateRange; dateRange?: DateRange;
filters?: Filter[]; filters?: Filter[];
impersonationId: string; impersonationId: string;
userId: string; userId: string;
withExcludedAccounts?: boolean; withExcludedAccounts?: boolean;
isAllocation?: boolean;
}): Promise<PortfolioDetails & { hasErrors: boolean }> { }): Promise<PortfolioDetails & { hasErrors: boolean }> {
userId = await this.getUserId(impersonationId, userId); userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
@ -373,8 +381,11 @@ export class PortfolioService {
transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT) transactionPoints[0]?.date ?? format(new Date(), DATE_FORMAT)
); );
const startDate = this.getStartDate(dateRange, portfolioStart); const startDate = this.getStartDate(dateRange, portfolioStart);
const currentPositions = const currentPositions = await portfolioCalculator.getCurrentPositions(
await portfolioCalculator.getCurrentPositions(startDate); startDate,
new Date(Date.now()),
!isAllocation
);
const cashDetails = await this.accountService.getCashDetails({ const cashDetails = await this.accountService.getCashDetails({
filters, filters,
@ -465,17 +476,23 @@ export class PortfolioService {
accounts, accounts,
holdings holdings
); );
let summary;
if (!isAllocation) {
summary = await this.getSummary({
impersonationId,
userCurrency,
userId,
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
emergencyFundPositionsValueInBaseCurrency:
this.getEmergencyFundPositionsValueInBaseCurrency({
holdings
})
});
}
const summary = await this.getSummary({ var netWorth =
impersonationId, summary?.netWorth ??
userCurrency, (await this.getNetWorth(impersonationId, userId, userCurrency));
userId,
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
emergencyFundPositionsValueInBaseCurrency:
this.getEmergencyFundPositionsValueInBaseCurrency({
holdings
})
});
return { return {
accounts, accounts,
@ -483,14 +500,15 @@ export class PortfolioService {
platforms, platforms,
summary, summary,
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: summary.netWorth filteredValueInPercentage: netWorth
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber() ? filteredValueInBaseCurrency.div(netWorth).toNumber()
: 0, : 0,
hasErrors: currentPositions.hasErrors, hasErrors: currentPositions.hasErrors,
totalValueInBaseCurrency: summary.netWorth totalValueInBaseCurrency: netWorth
}; };
} }
@LogPerformance
private handlePositions( private handlePositions(
currentPositions: CurrentPositions, currentPositions: CurrentPositions,
portfolioItemsNow: { [symbol: string]: TimelinePosition }, portfolioItemsNow: { [symbol: string]: TimelinePosition },
@ -567,6 +585,7 @@ export class PortfolioService {
} }
} }
@LogPerformance
private async handleCashPosition( private async handleCashPosition(
filters: Filter[], filters: Filter[],
isFilteredByAccount: boolean, isFilteredByAccount: boolean,
@ -592,6 +611,7 @@ export class PortfolioService {
} }
} }
@LogPerformance
private async handleEmergencyFunds( private async handleEmergencyFunds(
filters: Filter[], filters: Filter[],
cashDetails: CashDetails, cashDetails: CashDetails,
@ -647,6 +667,7 @@ export class PortfolioService {
return filteredValueInBaseCurrency; return filteredValueInBaseCurrency;
} }
@LogPerformance
private calculateMarketsAllocation( private calculateMarketsAllocation(
symbolProfile: EnhancedSymbolProfile, symbolProfile: EnhancedSymbolProfile,
markets: { markets: {
@ -719,6 +740,7 @@ export class PortfolioService {
} }
} }
@LogPerformance
public async getPosition( public async getPosition(
aDataSource: DataSource, aDataSource: DataSource,
aImpersonationId: string, aImpersonationId: string,
@ -1049,6 +1071,7 @@ export class PortfolioService {
} }
} }
@LogPerformance
public async getPositions({ public async getPositions({
dateRange = 'max', dateRange = 'max',
filters, filters,
@ -1192,6 +1215,7 @@ export class PortfolioService {
}; };
} }
@LogPerformance
public async getPerformance({ public async getPerformance({
dateRange = 'max', dateRange = 'max',
filters, filters,
@ -1386,6 +1410,7 @@ export class PortfolioService {
}; };
} }
@LogPerformance
public async getReport(impersonationId: string): Promise<PortfolioReport> { public async getReport(impersonationId: string): Promise<PortfolioReport> {
const userId = await this.getUserId(impersonationId, this.request.user.id); const userId = await this.getUserId(impersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
@ -1485,6 +1510,7 @@ export class PortfolioService {
}; };
} }
@LogPerformance
private async getCashPositions({ private async getCashPositions({
cashDetails, cashDetails,
userCurrency, userCurrency,
@ -1535,6 +1561,7 @@ export class PortfolioService {
return cashPositions; return cashPositions;
} }
@LogPerformance
private async getChart({ private async getChart({
dateRange = 'max', dateRange = 'max',
impersonationId, impersonationId,
@ -1599,6 +1626,7 @@ export class PortfolioService {
}; };
} }
@LogPerformance
private getDividendsByGroup({ private getDividendsByGroup({
dividends, dividends,
groupBy groupBy
@ -1661,6 +1689,7 @@ export class PortfolioService {
return dividendsByGroup; return dividendsByGroup;
} }
@LogPerformance
private getEmergencyFundPositionsValueInBaseCurrency({ private getEmergencyFundPositionsValueInBaseCurrency({
holdings holdings
}: { }: {
@ -1684,6 +1713,7 @@ export class PortfolioService {
return valueInBaseCurrencyOfEmergencyFundPositions.toNumber(); return valueInBaseCurrencyOfEmergencyFundPositions.toNumber();
} }
@LogPerformance
private getFees({ private getFees({
activities, activities,
date = new Date(0), date = new Date(0),
@ -1711,6 +1741,7 @@ export class PortfolioService {
); );
} }
@LogPerformance
private getInitialCashPosition({ private getInitialCashPosition({
balance, balance,
currency currency
@ -1743,6 +1774,7 @@ export class PortfolioService {
}; };
} }
@LogPerformance
private getStartDate(aDateRange: DateRange, portfolioStart: Date) { private getStartDate(aDateRange: DateRange, portfolioStart: Date) {
switch (aDateRange) { switch (aDateRange) {
case '1d': case '1d':
@ -1811,6 +1843,7 @@ export class PortfolioService {
return portfolioStart; return portfolioStart;
} }
@LogPerformance
private getStreaks({ private getStreaks({
investments, investments,
savingsRate savingsRate
@ -1833,6 +1866,43 @@ export class PortfolioService {
return { currentStreak, longestStreak }; 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({ private async getSummary({
balanceInBaseCurrency, balanceInBaseCurrency,
emergencyFundPositionsValueInBaseCurrency, emergencyFundPositionsValueInBaseCurrency,
@ -1848,11 +1918,26 @@ export class PortfolioService {
}): Promise<PortfolioSummary> { }): Promise<PortfolioSummary> {
userId = await this.getUserId(impersonationId, userId); userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
let performanceInformation: PortfolioPerformanceResponse = {
const performanceInformation = await this.getPerformance({ chart: [],
impersonationId, firstOrderDate: undefined,
userId 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({ const { activities } = await this.orderService.getOrders({
userCurrency, userCurrency,
@ -1871,6 +1956,13 @@ export class PortfolioService {
let totalSell = 0; let totalSell = 0;
let activitiesUsed: Activity[] = []; let activitiesUsed: Activity[] = [];
let ordersCount = 0; let ordersCount = 0;
let excludedAccountsAndActivities = 0;
const firstOrderDate = activities[0]?.date;
performanceInformation = await this.getPerformance({
impersonationId,
userId
});
for (let order of activities) { for (let order of activities) {
if (order.Account?.isExcluded ?? false) { if (order.Account?.isExcluded ?? false) {
excludedActivities.push(order); excludedActivities.push(order);
@ -1917,8 +2009,6 @@ export class PortfolioService {
) )
); );
const firstOrderDate = activitiesUsed[0]?.date;
const cash = new Big(balanceInBaseCurrency) const cash = new Big(balanceInBaseCurrency)
.minus(emergencyFund) .minus(emergencyFund)
.plus(emergencyFundPositionsValueInBaseCurrency) .plus(emergencyFundPositionsValueInBaseCurrency)
@ -1942,12 +2032,11 @@ export class PortfolioService {
currency: userCurrency, currency: userCurrency,
withExcludedAccounts: true withExcludedAccounts: true
}); });
const excludedBalanceInBaseCurrency = new Big( const excludedBalanceInBaseCurrency = new Big(
cashDetailsWithExcludedAccounts.balanceInBaseCurrency cashDetailsWithExcludedAccounts.balanceInBaseCurrency
).minus(balanceInBaseCurrency); ).minus(balanceInBaseCurrency);
const excludedAccountsAndActivities = excludedBalanceInBaseCurrency excludedAccountsAndActivities = excludedBalanceInBaseCurrency
.plus(totalOfExcludedActivities) .plus(totalOfExcludedActivities)
.toNumber(); .toNumber();
@ -2003,6 +2092,7 @@ export class PortfolioService {
}; };
} }
@LogPerformance
private getSumOfActivityType({ private getSumOfActivityType({
activities, activities,
activityType, activityType,
@ -2036,6 +2126,7 @@ export class PortfolioService {
); );
} }
@LogPerformance
private async getTransactionPoints({ private async getTransactionPoints({
filters, filters,
includeDrafts = false, includeDrafts = false,
@ -2111,6 +2202,7 @@ export class PortfolioService {
return impersonationUserId || aUserId; return impersonationUserId || aUserId;
} }
@LogPerformance
private async getValueOfAccountsAndPlatforms({ private async getValueOfAccountsAndPlatforms({
filters = [], filters = [],
orders, orders,
@ -2261,6 +2353,7 @@ export class PortfolioService {
return { accounts, platforms }; return { accounts, platforms };
} }
@LogPerformance
private mergeHistoricalDataItems( private mergeHistoricalDataItems(
accountBalanceItems: HistoricalDataItem[], accountBalanceItems: HistoricalDataItem[],
performanceChartItems: HistoricalDataItem[] performanceChartItems: HistoricalDataItem[]

32
apps/api/src/main.ts

@ -5,6 +5,7 @@ import type { NestExpressApplication } from '@nestjs/platform-express';
import * as bodyParser from 'body-parser'; import * as bodyParser from 'body-parser';
import helmet from 'helmet'; import helmet from 'helmet';
import { LoggingInterceptor } from './aop/logging.interceptor';
import { AppModule } from './app/app.module'; import { AppModule } from './app/app.module';
import { environment } from './environments/environment'; import { environment } from './environments/environment';
import { HtmlTemplateMiddleware } from './middlewares/html-template.middleware'; import { HtmlTemplateMiddleware } from './middlewares/html-template.middleware';
@ -13,10 +14,34 @@ async function bootstrap() {
const configApp = await NestFactory.create(AppModule); const configApp = await NestFactory.create(AppModule);
const configService = configApp.get<ConfigService>(ConfigService); const configService = configApp.get<ConfigService>(ConfigService);
let logLevelArray = [];
let logLevel = configService.get<string>('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<NestExpressApplication>(AppModule, { const app = await NestFactory.create<NestExpressApplication>(AppModule, {
logger: environment.production logger: logLevelArray
? ['error', 'log', 'warn']
: ['debug', 'error', 'log', 'verbose', 'warn']
}); });
app.enableCors(); app.enableCors();
@ -25,6 +50,7 @@ async function bootstrap() {
type: VersioningType.URI type: VersioningType.URI
}); });
app.setGlobalPrefix('api', { exclude: ['sitemap.xml'] }); app.setGlobalPrefix('api', { exclude: ['sitemap.xml'] });
app.useGlobalInterceptors(new LoggingInterceptor());
app.useGlobalPipes( app.useGlobalPipes(
new ValidationPipe({ new ValidationPipe({
forbidNonWhitelisted: true, forbidNonWhitelisted: true,

1
apps/api/src/services/configuration/configuration.service.ts

@ -22,6 +22,7 @@ export class ConfigurationService {
API_KEY_RAPID_API: str({ default: '' }), API_KEY_RAPID_API: str({ default: '' }),
CACHE_QUOTES_TTL: num({ default: 1 }), CACHE_QUOTES_TTL: num({ default: 1 }),
CACHE_TTL: num({ default: 1 }), CACHE_TTL: num({ default: 1 }),
LOG_LEVEL: str({ default: '' }),
DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }), DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }),
DATA_SOURCE_IMPORT: str({ default: DataSource.YAHOO }), DATA_SOURCE_IMPORT: str({ default: DataSource.YAHOO }),
DATA_SOURCES: json({ DATA_SOURCES: json({

2
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 { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
@ -332,6 +333,7 @@ export class DataProviderService {
return result; return result;
} }
@LogPerformance
public async getQuotes({ public async getQuotes({
items, items,
requestTimeout, requestTimeout,

2
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 { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { import {
@ -39,6 +40,7 @@ export class SymbolProfileService {
}); });
} }
@LogPerformance
public async getSymbolProfiles( public async getSymbolProfiles(
aUniqueAssets: UniqueAsset[] aUniqueAssets: UniqueAsset[]
): Promise<EnhancedSymbolProfile[]> { ): Promise<EnhancedSymbolProfile[]> {

5
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -205,7 +205,10 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
private fetchPortfolioDetails() { private fetchPortfolioDetails() {
return this.dataService.fetchPortfolioDetails({ return this.dataService.fetchPortfolioDetails({
filters: this.userService.getFilters() filters: this.userService.getFilters(),
parameters: {
isAllocation: true
}
}); });
} }

16
apps/client/src/app/services/data.service.ts

@ -389,13 +389,25 @@ export class DataService {
} }
public fetchPortfolioDetails({ public fetchPortfolioDetails({
filters filters,
parameters
}: { }: {
filters?: Filter[]; filters?: Filter[];
parameters?: {
[param: string]:
| string
| number
| boolean
| readonly (string | number | boolean)[];
};
} = {}): Observable<PortfolioDetails> { } = {}): Observable<PortfolioDetails> {
let params = this.buildFiltersAsQueryParams({ filters }).appendAll(
parameters
);
return this.http return this.http
.get<any>('/api/v1/portfolio/details', { .get<any>('/api/v1/portfolio/details', {
params: this.buildFiltersAsQueryParams({ filters }) params
}) })
.pipe( .pipe(
map((response) => { map((response) => {

Loading…
Cancel
Save