Feature/switch to performance calculations with currency effects (#3039)

* Switch to performance calculations with currency effects

* Improve value redaction in portfolio details endpoint

* Update changelog
pull/3043/head
Thomas Kaul 1 year ago committed by GitHub
parent c002e37285
commit 2e9d40c201
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Switched the performance calculations to take the currency effects into account
- Removed the `isDefault` flag from the `Account` database schema - Removed the `isDefault` flag from the `Account` database schema
- Exposed the database index of _Redis_ as an environment variable (`REDIS_DB`) - Exposed the database index of _Redis_ as an environment variable (`REDIS_DB`)
- Improved the language localization for German (`de`) - Improved the language localization for German (`de`)

@ -118,27 +118,23 @@ export class PortfolioController {
this.userService.isRestrictedView(this.request.user) this.userService.isRestrictedView(this.request.user)
) { ) {
const totalInvestment = Object.values(holdings) const totalInvestment = Object.values(holdings)
.map((portfolioPosition) => { .map(({ investment }) => {
return portfolioPosition.investment; return investment;
}) })
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
const totalValue = Object.values(holdings) const totalValue = Object.values(holdings)
.map((portfolioPosition) => { .filter(({ assetClass, assetSubClass }) => {
return this.exchangeRateDataService.toCurrency( return assetClass !== 'CASH' && assetSubClass !== 'CASH';
portfolioPosition.quantity * portfolioPosition.marketPrice, })
portfolioPosition.currency, .map(({ valueInBaseCurrency }) => {
this.request.user.Settings.settings.baseCurrency return valueInBaseCurrency;
);
}) })
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
for (const [symbol, portfolioPosition] of Object.entries(holdings)) { for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
portfolioPosition.grossPerformance = null;
portfolioPosition.investment = portfolioPosition.investment =
portfolioPosition.investment / totalInvestment; portfolioPosition.investment / totalInvestment;
portfolioPosition.netPerformance = null;
portfolioPosition.quantity = null;
portfolioPosition.valueInPercentage = portfolioPosition.valueInPercentage =
portfolioPosition.valueInBaseCurrency / totalValue; portfolioPosition.valueInBaseCurrency / totalValue;
} }

@ -529,12 +529,20 @@ export class PortfolioService {
grossPerformance: item.grossPerformance?.toNumber() ?? 0, grossPerformance: item.grossPerformance?.toNumber() ?? 0,
grossPerformancePercent: grossPerformancePercent:
item.grossPerformancePercentage?.toNumber() ?? 0, item.grossPerformancePercentage?.toNumber() ?? 0,
grossPerformancePercentWithCurrencyEffect:
item.grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
grossPerformanceWithCurrencyEffect:
item.grossPerformanceWithCurrencyEffect?.toNumber() ?? 0,
investment: item.investment.toNumber(), investment: item.investment.toNumber(),
marketPrice: item.marketPrice, marketPrice: item.marketPrice,
marketState: dataProviderResponse?.marketState ?? 'delayed', marketState: dataProviderResponse?.marketState ?? 'delayed',
name: symbolProfile.name, name: symbolProfile.name,
netPerformance: item.netPerformance?.toNumber() ?? 0, netPerformance: item.netPerformance?.toNumber() ?? 0,
netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0, netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0,
netPerformancePercentWithCurrencyEffect:
item.netPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0,
netPerformanceWithCurrencyEffect:
item.netPerformanceWithCurrencyEffect?.toNumber() ?? 0,
quantity: item.quantity.toNumber(), quantity: item.quantity.toNumber(),
sectors: symbolProfile.sectors, sectors: symbolProfile.sectors,
symbol: item.symbol, symbol: item.symbol,
@ -1600,12 +1608,16 @@ export class PortfolioService {
dateOfFirstActivity: undefined, dateOfFirstActivity: undefined,
grossPerformance: 0, grossPerformance: 0,
grossPerformancePercent: 0, grossPerformancePercent: 0,
grossPerformancePercentWithCurrencyEffect: 0,
grossPerformanceWithCurrencyEffect: 0,
investment: balance, investment: balance,
marketPrice: 0, marketPrice: 0,
marketState: 'open', marketState: 'open',
name: currency, name: currency,
netPerformance: 0, netPerformance: 0,
netPerformancePercent: 0, netPerformancePercent: 0,
netPerformancePercentWithCurrencyEffect: 0,
netPerformanceWithCurrencyEffect: 0,
quantity: 0, quantity: 0,
sectors: [], sectors: [],
symbol: currency, symbol: currency,
@ -1814,9 +1826,25 @@ export class PortfolioService {
}) })
?.toNumber(); ?.toNumber();
const annualizedPerformancePercentWithCurrencyEffect =
new PortfolioCalculator({
currency: userCurrency,
currentRateService: this.currentRateService,
exchangeRateDataService: this.exchangeRateDataService,
orders: []
})
.getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent: new Big(
performanceInformation.performance.currentNetPerformancePercentWithCurrencyEffect
)
})
?.toNumber();
return { return {
...performanceInformation.performance, ...performanceInformation.performance,
annualizedPerformancePercent, annualizedPerformancePercent,
annualizedPerformancePercentWithCurrencyEffect,
cash, cash,
dividend, dividend,
excludedAccountsAndActivities, excludedAccountsAndActivities,

@ -51,8 +51,10 @@ export class RedactValuesInResponseInterceptor<T>
'feeInBaseCurrency', 'feeInBaseCurrency',
'filteredValueInBaseCurrency', 'filteredValueInBaseCurrency',
'grossPerformance', 'grossPerformance',
'grossPerformanceWithCurrencyEffect',
'investment', 'investment',
'netPerformance', 'netPerformance',
'netPerformanceWithCurrencyEffect',
'quantity', 'quantity',
'symbolMapping', 'symbolMapping',
'totalBalanceInBaseCurrency', 'totalBalanceInBaseCurrency',

@ -154,8 +154,8 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchPositions({ range: this.user?.settings?.dateRange }) .fetchPositions({ range: this.user?.settings?.dateRange })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => { .subscribe(({ positions }) => {
this.positions = response.positions; this.positions = positions;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });

@ -127,10 +127,10 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
this.isLoadingPerformance = false; this.isLoadingPerformance = false;
this.historicalDataItems = chart.map( this.historicalDataItems = chart.map(
({ date, netPerformanceInPercentage }) => { ({ date, netPerformanceInPercentageWithCurrencyEffect }) => {
return { return {
date, date,
value: netPerformanceInPercentage value: netPerformanceInPercentageWithCurrencyEffect
}; };
} }
); );

@ -40,7 +40,11 @@
[colorizeSign]="true" [colorizeSign]="true"
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : performance?.currentNetPerformance" [value]="
isLoading
? undefined
: performance?.currentNetPerformanceWithCurrencyEffect
"
/> />
</div> </div>
<div class="col"> <div class="col">
@ -49,7 +53,9 @@
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]=" [value]="
isLoading ? undefined : performance?.currentNetPerformancePercent isLoading
? undefined
: performance?.currentNetPerformancePercentWithCurrencyEffect
" "
/> />
</div> </div>

@ -63,7 +63,8 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
} else if (this.showDetails === false) { } else if (this.showDetails === false) {
new CountUp( new CountUp(
'value', 'value',
this.performance?.currentNetPerformancePercent * 100, this.performance?.currentNetPerformancePercentWithCurrencyEffect *
100,
{ {
decimal: getNumberFormatDecimal(this.locale), decimal: getNumberFormatDecimal(this.locale),
decimalPlaces: 2, decimalPlaces: 2,

@ -64,7 +64,11 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.currentGrossPerformance" [value]="
isLoading
? undefined
: summary?.currentGrossPerformanceWithCurrencyEffect
"
/> />
</div> </div>
</div> </div>
@ -85,7 +89,9 @@
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]=" [value]="
isLoading ? undefined : summary?.currentGrossPerformancePercent isLoading
? undefined
: summary?.currentGrossPerformancePercentWithCurrencyEffect
" "
/> />
</div> </div>
@ -114,7 +120,11 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="isLoading ? undefined : summary?.currentNetPerformance" [value]="
isLoading
? undefined
: summary?.currentNetPerformanceWithCurrencyEffect
"
/> />
</div> </div>
</div> </div>
@ -134,7 +144,11 @@
[colorizeSign]="true" [colorizeSign]="true"
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : summary?.currentNetPerformancePercent" [value]="
isLoading
? undefined
: summary?.currentNetPerformancePercentWithCurrencyEffect
"
/> />
</div> </div>
</div> </div>
@ -283,7 +297,11 @@
[colorizeSign]="true" [colorizeSign]="true"
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : summary?.annualizedPerformancePercent" [value]="
isLoading
? undefined
: summary?.annualizedPerformancePercentWithCurrencyEffect
"
/> />
</div> </div>
</div> </div>

@ -50,15 +50,13 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
public dividendInBaseCurrency: number; public dividendInBaseCurrency: number;
public feeInBaseCurrency: number; public feeInBaseCurrency: number;
public firstBuyDate: string; public firstBuyDate: string;
public grossPerformance: number;
public grossPerformancePercent: number;
public historicalDataItems: LineChartItem[]; public historicalDataItems: LineChartItem[];
public investment: number; public investment: number;
public marketPrice: number; public marketPrice: number;
public maxPrice: number; public maxPrice: number;
public minPrice: number; public minPrice: number;
public netPerformance: number; public netPerformancePercentWithCurrencyEffect: number;
public netPerformancePercent: number; public netPerformanceWithCurrencyEffect: number;
public quantity: number; public quantity: number;
public quantityPrecision = 2; public quantityPrecision = 2;
public reportDataGlitchMail: string; public reportDataGlitchMail: string;
@ -99,15 +97,13 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
dividendInBaseCurrency, dividendInBaseCurrency,
feeInBaseCurrency, feeInBaseCurrency,
firstBuyDate, firstBuyDate,
grossPerformance,
grossPerformancePercent,
historicalData, historicalData,
investment, investment,
marketPrice, marketPrice,
maxPrice, maxPrice,
minPrice, minPrice,
netPerformance, netPerformancePercentWithCurrencyEffect,
netPerformancePercent, netPerformanceWithCurrencyEffect,
orders, orders,
quantity, quantity,
SymbolProfile, SymbolProfile,
@ -125,8 +121,6 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.dividendInBaseCurrency = dividendInBaseCurrency; this.dividendInBaseCurrency = dividendInBaseCurrency;
this.feeInBaseCurrency = feeInBaseCurrency; this.feeInBaseCurrency = feeInBaseCurrency;
this.firstBuyDate = firstBuyDate; this.firstBuyDate = firstBuyDate;
this.grossPerformance = grossPerformance;
this.grossPerformancePercent = grossPerformancePercent;
this.historicalDataItems = historicalData.map( this.historicalDataItems = historicalData.map(
(historicalDataItem) => { (historicalDataItem) => {
this.benchmarkDataItems.push({ this.benchmarkDataItems.push({
@ -144,8 +138,10 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.marketPrice = marketPrice; this.marketPrice = marketPrice;
this.maxPrice = maxPrice; this.maxPrice = maxPrice;
this.minPrice = minPrice; this.minPrice = minPrice;
this.netPerformance = netPerformance; this.netPerformancePercentWithCurrencyEffect =
this.netPerformancePercent = netPerformancePercent; netPerformancePercentWithCurrencyEffect;
this.netPerformanceWithCurrencyEffect =
netPerformanceWithCurrencyEffect;
this.quantity = quantity; this.quantity = quantity;
this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=Ghostfolio Data Glitch Report&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${SymbolProfile?.symbol}%0DData Source: ${SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`; this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=Ghostfolio Data Glitch Report&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${SymbolProfile?.symbol}%0DData Source: ${SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`;
this.sectors = {}; this.sectors = {};

@ -44,7 +44,7 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="data.locale" [locale]="data.locale"
[unit]="data.baseCurrency" [unit]="data.baseCurrency"
[value]="netPerformance" [value]="netPerformanceWithCurrencyEffect"
>Change</gf-value >Change</gf-value
> >
</div> </div>
@ -55,7 +55,7 @@
[colorizeSign]="true" [colorizeSign]="true"
[isPercent]="true" [isPercent]="true"
[locale]="data.locale" [locale]="data.locale"
[value]="netPerformancePercent" [value]="netPerformancePercentWithCurrencyEffect"
>Performance</gf-value >Performance</gf-value
> >
</div> </div>

@ -17,7 +17,7 @@
[isLoading]="isLoading" [isLoading]="isLoading"
[marketState]="position?.marketState" [marketState]="position?.marketState"
[range]="range" [range]="range"
[value]="position?.netPerformancePercentage" [value]="position?.netPerformancePercentageWithCurrencyEffect"
/> />
</div> </div>
<div *ngIf="isLoading" class="flex-grow-1"> <div *ngIf="isLoading" class="flex-grow-1">
@ -49,13 +49,13 @@
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[unit]="baseCurrency" [unit]="baseCurrency"
[value]="position?.netPerformance" [value]="position?.netPerformanceWithCurrencyEffect"
/> />
<gf-value <gf-value
[colorizeSign]="true" [colorizeSign]="true"
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="position?.netPerformancePercentage" [value]="position?.netPerformancePercentageWithCurrencyEffect"
/> />
</div> </div>
</div> </div>

@ -270,23 +270,28 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
index, index,
{ {
date, date,
netPerformanceInPercentage, netPerformanceInPercentageWithCurrencyEffect,
totalInvestment, totalInvestmentValueWithCurrencyEffect,
value, valueInPercentage,
valueInPercentage valueWithCurrencyEffect
} }
] of chart.entries()) { ] of chart.entries()) {
if (index > 0 || this.user?.settings?.dateRange === 'max') { if (index > 0 || this.user?.settings?.dateRange === 'max') {
// Ignore first item where value is 0 // Ignore first item where value is 0
this.investments.push({ date, investment: totalInvestment }); this.investments.push({
date,
investment: totalInvestmentValueWithCurrencyEffect
});
this.performanceDataItems.push({ this.performanceDataItems.push({
date, date,
value: isNumber(value) ? value : valueInPercentage value: isNumber(valueWithCurrencyEffect)
? valueWithCurrencyEffect
: valueInPercentage
}); });
} }
this.performanceDataItemsInPercentage.push({ this.performanceDataItemsInPercentage.push({
date, date,
value: netPerformanceInPercentage value: netPerformanceInPercentageWithCurrencyEffect
}); });
} }
@ -305,10 +310,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ positions }) => { .subscribe(({ positions }) => {
const positionsSorted = sortBy( const positionsSorted = sortBy(
positions.filter(({ netPerformancePercentage }) => { positions.filter(({ netPerformancePercentageWithCurrencyEffect }) => {
return isNumber(netPerformancePercentage); return isNumber(netPerformancePercentageWithCurrencyEffect);
}), }),
'netPerformancePercentage' 'netPerformancePercentageWithCurrencyEffect'
).reverse(); ).reverse();
this.top3 = positionsSorted.slice(0, 3); this.top3 = positionsSorted.slice(0, 3);

@ -17,6 +17,8 @@ export interface PortfolioPosition {
exchange?: string; exchange?: string;
grossPerformance: number; grossPerformance: number;
grossPerformancePercent: number; grossPerformancePercent: number;
grossPerformancePercentWithCurrencyEffect: number;
grossPerformanceWithCurrencyEffect: number;
investment: number; investment: number;
marketChange?: number; marketChange?: number;
marketChangePercent?: number; marketChangePercent?: number;
@ -27,6 +29,8 @@ export interface PortfolioPosition {
name: string; name: string;
netPerformance: number; netPerformance: number;
netPerformancePercent: number; netPerformancePercent: number;
netPerformancePercentWithCurrencyEffect: number;
netPerformanceWithCurrencyEffect: number;
quantity: number; quantity: number;
sectors: Sector[]; sectors: Sector[];
symbol: string; symbol: string;

@ -2,6 +2,7 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
export interface PortfolioSummary extends PortfolioPerformance { export interface PortfolioSummary extends PortfolioPerformance {
annualizedPerformancePercent: number; annualizedPerformancePercent: number;
annualizedPerformancePercentWithCurrencyEffect: number;
cash: number; cash: number;
committedFunds: number; committedFunds: number;
dividend: number; dividend: number;

@ -18,6 +18,8 @@ export interface Position {
name?: string; name?: string;
netPerformance?: number; netPerformance?: number;
netPerformancePercentage?: number; netPerformancePercentage?: number;
netPerformancePercentageWithCurrencyEffect?: number;
netPerformanceWithCurrencyEffect?: number;
quantity: number; quantity: number;
symbol: string; symbol: string;
transactionCount: number; transactionCount: number;

@ -114,7 +114,7 @@
*matHeaderCellDef *matHeaderCellDef
class="justify-content-end px-1" class="justify-content-end px-1"
mat-header-cell mat-header-cell
mat-sort-header="netPerformancePercent" mat-sort-header="netPerformancePercentWithCurrencyEffect"
> >
<span class="d-none d-sm-block" i18n>Performance</span> <span class="d-none d-sm-block" i18n>Performance</span>
<span class="d-block d-sm-none" title="Performance">±</span> <span class="d-block d-sm-none" title="Performance">±</span>
@ -125,7 +125,11 @@
[colorizeSign]="true" [colorizeSign]="true"
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : element.netPerformancePercent" [value]="
isLoading
? undefined
: element.netPerformancePercentWithCurrencyEffect
"
/> />
</div> </div>
</td> </td>

Loading…
Cancel
Save