Browse Source

Refactored performance calculations

- Avoiding multiple iterations over same array
- Some Refactorings for readability
pull/5027/head
Daniel Devaud 2 years ago
parent
commit
c069c0ffa4
  1. 56
      apps/api/src/app/portfolio/portfolio-calculator.ts
  2. 100
      apps/api/src/app/portfolio/portfolio.controller.ts
  3. 227
      apps/api/src/app/portfolio/portfolio.service.ts

56
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -3,6 +3,7 @@ import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfac
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import { import {
DataProviderInfo, DataProviderInfo,
HistoricalDataItem,
ResponseError, ResponseError,
TimelinePosition TimelinePosition
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -277,46 +278,29 @@ export class PortfolioCalculator {
}; };
} }
for (const currentDate of dates) { return dates.map((date) => {
const dateString = format(currentDate, DATE_FORMAT); const dateString = format(date, DATE_FORMAT);
let totalCurrentValue = new Big(0);
let totalInvestmentValue = new Big(0);
let maxTotalInvestmentValue = new Big(0);
let totalNetPerformanceValue = new Big(0);
for (const symbol of Object.keys(valuesBySymbol)) { for (const symbol of Object.keys(valuesBySymbol)) {
const symbolValues = valuesBySymbol[symbol]; const symbolValues = valuesBySymbol[symbol];
const currentValue = totalCurrentValue = totalCurrentValue.plus(
symbolValues.currentValues?.[dateString] ?? new Big(0); symbolValues.currentValues?.[dateString] ?? new Big(0)
const investmentValue = );
symbolValues.investmentValues?.[dateString] ?? new Big(0); totalInvestmentValue = totalInvestmentValue.plus(
const maxInvestmentValue = symbolValues.investmentValues?.[dateString] ?? new Big(0)
symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0); );
const netPerformanceValue = maxTotalInvestmentValue = maxTotalInvestmentValue.plus(
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0); symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0)
);
valuesByDate[dateString] = { totalNetPerformanceValue = totalNetPerformanceValue.plus(
totalCurrentValue: ( symbolValues.netPerformanceValues?.[dateString] ?? new Big(0)
valuesByDate[dateString]?.totalCurrentValue ?? new Big(0) );
).add(currentValue),
totalInvestmentValue: (
valuesByDate[dateString]?.totalInvestmentValue ?? new Big(0)
).add(investmentValue),
maxTotalInvestmentValue: (
valuesByDate[dateString]?.maxTotalInvestmentValue ?? new Big(0)
).add(maxInvestmentValue),
totalNetPerformanceValue: (
valuesByDate[dateString]?.totalNetPerformanceValue ?? new Big(0)
).add(netPerformanceValue)
};
} }
}
return Object.entries(valuesByDate).map(([date, values]) => {
const {
maxTotalInvestmentValue,
totalCurrentValue,
totalInvestmentValue,
totalNetPerformanceValue
} = values;
const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0) const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0)
? 0 ? 0
: totalNetPerformanceValue : totalNetPerformanceValue
@ -325,7 +309,7 @@ export class PortfolioCalculator {
.toNumber(); .toNumber();
return { return {
date, date: dateString,
netPerformanceInPercentage, netPerformanceInPercentage,
netPerformance: totalNetPerformanceValue.toNumber(), netPerformance: totalNetPerformanceValue.toNumber(),
totalInvestment: totalInvestmentValue.toNumber(), totalInvestment: totalInvestmentValue.toNumber(),

100
apps/api/src/app/portfolio/portfolio.controller.ts

@ -111,21 +111,38 @@ export class PortfolioController {
impersonationId || impersonationId ||
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const totalInvestment = Object.values(holdings) let investmentTuple: [number, number] = [0, 0];
.map((portfolioPosition) => { for (let holding of Object.entries(holdings)) {
return portfolioPosition.investment; var portfolioPosition = holding[1];
}) investmentTuple[0] += portfolioPosition.investment;
.reduce((a, b) => a + b, 0); investmentTuple[1] += this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice,
const totalValue = Object.values(holdings) portfolioPosition.currency,
.map((portfolioPosition) => { this.request.user.Settings.settings.baseCurrency
return this.exchangeRateDataService.toCurrency( );
portfolioPosition.quantity * portfolioPosition.marketPrice, }
portfolioPosition.currency, const totalInvestment = investmentTuple[0];
this.request.user.Settings.settings.baseCurrency
); const totalValue = investmentTuple[1];
})
.reduce((a, b) => a + b, 0); if (hasDetails === false) {
portfolioSummary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'dividend',
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
'items',
'liabilities',
'netWorth',
'totalBuy',
'totalSell'
]);
}
for (const [symbol, portfolioPosition] of Object.entries(holdings)) { for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
portfolioPosition.grossPerformance = null; portfolioPosition.grossPerformance = null;
@ -135,6 +152,24 @@ export class PortfolioController {
portfolioPosition.quantity = null; portfolioPosition.quantity = null;
portfolioPosition.valueInPercentage = portfolioPosition.valueInPercentage =
portfolioPosition.value / totalValue; portfolioPosition.value / totalValue;
(portfolioPosition.assetClass = hasDetails
? portfolioPosition.assetClass
: undefined),
(portfolioPosition.assetSubClass = hasDetails
? portfolioPosition.assetSubClass
: undefined),
(portfolioPosition.countries = hasDetails
? portfolioPosition.countries
: []),
(portfolioPosition.currency = hasDetails
? portfolioPosition.currency
: undefined),
(portfolioPosition.markets = hasDetails
? portfolioPosition.markets
: undefined),
(portfolioPosition.sectors = hasDetails
? portfolioPosition.sectors
: []);
} }
for (const [name, { valueInBaseCurrency }] of Object.entries(accounts)) { for (const [name, { valueInBaseCurrency }] of Object.entries(accounts)) {
@ -146,41 +181,6 @@ export class PortfolioController {
} }
} }
if (
hasDetails === false ||
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
portfolioSummary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'dividend',
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
'items',
'liabilities',
'netWorth',
'totalBuy',
'totalSell'
]);
}
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
holdings[symbol] = {
...portfolioPosition,
assetClass: hasDetails ? portfolioPosition.assetClass : undefined,
assetSubClass: hasDetails ? portfolioPosition.assetSubClass : undefined,
countries: hasDetails ? portfolioPosition.countries : [],
currency: hasDetails ? portfolioPosition.currency : undefined,
markets: hasDetails ? portfolioPosition.markets : undefined,
sectors: hasDetails ? portfolioPosition.sectors : []
};
}
return { return {
accounts, accounts,
filteredValueInBaseCurrency, filteredValueInBaseCurrency,

227
apps/api/src/app/portfolio/portfolio.service.ts

@ -522,11 +522,9 @@ export class PortfolioService {
} }
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
for (const position of currentPositions.positions) {
portfolioItemsNow[position.symbol] = position;
}
for (const item of currentPositions.positions) { for (const item of currentPositions.positions) {
portfolioItemsNow[item.symbol] = item;
if (item.quantity.lte(0)) { if (item.quantity.lte(0)) {
// Ignore positions without any quantity // Ignore positions without any quantity
continue; continue;
@ -542,21 +540,7 @@ export class PortfolioService {
otherMarkets: 0 otherMarkets: 0
}; };
for (const country of symbolProfile.countries) { this.calculateMarketsAllocation(symbolProfile, markets);
if (developedMarkets.includes(country.code)) {
markets.developedMarkets = new Big(markets.developedMarkets)
.plus(country.weight)
.toNumber();
} else if (emergingMarkets.includes(country.code)) {
markets.emergingMarkets = new Big(markets.emergingMarkets)
.plus(country.weight)
.toNumber();
} else {
markets.otherMarkets = new Big(markets.otherMarkets)
.plus(country.weight)
.toNumber();
}
}
holdings[item.symbol] = { holdings[item.symbol] = {
markets, markets,
@ -587,6 +571,68 @@ export class PortfolioService {
}; };
} }
await this.handleCashPosition(
filters,
isFilteredByAccount,
cashDetails,
userCurrency,
filteredValueInBaseCurrency,
holdings
);
const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({
filters,
orders,
portfolioItemsNow,
userCurrency,
userId,
withExcludedAccounts
});
filteredValueInBaseCurrency = await this.handleEmergencyFunds(
filters,
cashDetails,
userCurrency,
filteredValueInBaseCurrency,
emergencyFund,
orders,
accounts,
holdings
);
const summary = await this.getSummary({
impersonationId,
userCurrency,
userId,
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
emergencyFundPositionsValueInBaseCurrency:
this.getEmergencyFundPositionsValueInBaseCurrency({
activities: orders
})
});
return {
accounts,
holdings,
platforms,
summary,
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: summary.netWorth
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
: 0,
hasErrors: currentPositions.hasErrors,
totalValueInBaseCurrency: summary.netWorth
};
}
private async handleCashPosition(
filters: Filter[],
isFilteredByAccount: boolean,
cashDetails: CashDetails,
userCurrency: string,
filteredValueInBaseCurrency: Big,
holdings: { [symbol: string]: PortfolioPosition }
) {
const isFilteredByCash = filters?.some((filter) => { const isFilteredByCash = filters?.some((filter) => {
return filter.type === 'ASSET_CLASS' && filter.id === 'CASH'; return filter.type === 'ASSET_CLASS' && filter.id === 'CASH';
}); });
@ -602,16 +648,26 @@ export class PortfolioService {
holdings[symbol] = cashPositions[symbol]; holdings[symbol] = cashPositions[symbol];
} }
} }
}
const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({ private async handleEmergencyFunds(
filters, filters: Filter[],
orders, cashDetails: CashDetails,
portfolioItemsNow, userCurrency: string,
userCurrency, filteredValueInBaseCurrency: Big,
userId, emergencyFund: Big,
withExcludedAccounts orders: Activity[],
}); accounts: {
[id: string]: {
balance: number;
currency: string;
name: string;
valueInBaseCurrency: number;
valueInPercentage?: number;
};
},
holdings: { [symbol: string]: PortfolioPosition }
) {
if ( if (
filters?.length === 1 && filters?.length === 1 &&
filters[0].id === EMERGENCY_FUND_TAG_ID && filters[0].id === EMERGENCY_FUND_TAG_ID &&
@ -646,30 +702,32 @@ export class PortfolioService {
value: emergencyFundInCash value: emergencyFundInCash
}; };
} }
return filteredValueInBaseCurrency;
}
const summary = await this.getSummary({ private calculateMarketsAllocation(
impersonationId, symbolProfile: EnhancedSymbolProfile,
userCurrency, markets: {
userId, developedMarkets: number;
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency, emergingMarkets: number;
emergencyFundPositionsValueInBaseCurrency: otherMarkets: number;
this.getEmergencyFundPositionsValueInBaseCurrency({ }
activities: orders ) {
}) for (const country of symbolProfile.countries) {
}); if (developedMarkets.includes(country.code)) {
markets.developedMarkets = new Big(markets.developedMarkets)
return { .plus(country.weight)
accounts, .toNumber();
holdings, } else if (emergingMarkets.includes(country.code)) {
platforms, markets.emergingMarkets = new Big(markets.emergingMarkets)
summary, .plus(country.weight)
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(), .toNumber();
filteredValueInPercentage: summary.netWorth } else {
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber() markets.otherMarkets = new Big(markets.otherMarkets)
: 0, .plus(country.weight)
hasErrors: currentPositions.hasErrors, .toNumber();
totalValueInBaseCurrency: summary.netWorth }
}; }
} }
public async getPosition( public async getPosition(
@ -1607,38 +1665,63 @@ export class PortfolioService {
userId userId
}); });
const activities = await this.orderService.getOrders({ const ordersRaw = await this.orderService.getOrders({
userCurrency, userCurrency,
userId userId,
}); withExcludedAccounts: true
const excludedActivities = (
await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
})
).filter(({ Account: account }) => {
return account?.isExcluded ?? false;
}); });
const activities: Activity[] = [];
const excludedActivities: Activity[] = [];
let dividend = 0;
let fees = 0;
let items = 0;
let liabilities = 0;
let totalBuy = 0;
let totalSell = 0;
for (let order of ordersRaw) {
if (order.Account?.isExcluded ?? false) {
excludedActivities.push(order);
} else {
activities.push(order);
fees += this.exchangeRateDataService.toCurrency(
order.fee,
order.SymbolProfile.currency,
userCurrency
);
let amount = this.exchangeRateDataService.toCurrency(
new Big(order.quantity).mul(order.unitPrice).toNumber(),
order.SymbolProfile.currency,
userCurrency
);
switch (order.type) {
case 'DIVIDEND':
dividend += amount;
break;
case 'ITEM':
items += amount;
break;
case 'SELL':
totalSell += amount;
break;
case 'BUY':
totalBuy += amount;
break;
case 'LIABILITY':
liabilities += amount;
}
}
}
const dividend = this.getDividend({
activities,
userCurrency
}).toNumber();
const emergencyFund = new Big( const emergencyFund = new Big(
Math.max( Math.max(
emergencyFundPositionsValueInBaseCurrency, emergencyFundPositionsValueInBaseCurrency,
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0 (user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
) )
); );
const fees = this.getFees({ activities, userCurrency }).toNumber();
const firstOrderDate = activities[0]?.date;
const items = this.getItems(activities).toNumber();
const liabilities = this.getLiabilities(activities).toNumber();
const totalBuy = this.getTotalByType(activities, userCurrency, 'BUY'); const firstOrderDate = activities[0]?.date;
const totalSell = this.getTotalByType(activities, userCurrency, 'SELL');
const cash = new Big(balanceInBaseCurrency) const cash = new Big(balanceInBaseCurrency)
.minus(emergencyFund) .minus(emergencyFund)

Loading…
Cancel
Save