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
- Switched the performance calculations to take the currency effects into account
- Removed the `isDefault` flag from the `Account` database schema
- Exposed the database index of _Redis_ as an environment variable (`REDIS_DB`)
- Improved the language localization for German (`de`)

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

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

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

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

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

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

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

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

@ -50,15 +50,13 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
public dividendInBaseCurrency: number;
public feeInBaseCurrency: number;
public firstBuyDate: string;
public grossPerformance: number;
public grossPerformancePercent: number;
public historicalDataItems: LineChartItem[];
public investment: number;
public marketPrice: number;
public maxPrice: number;
public minPrice: number;
public netPerformance: number;
public netPerformancePercent: number;
public netPerformancePercentWithCurrencyEffect: number;
public netPerformanceWithCurrencyEffect: number;
public quantity: number;
public quantityPrecision = 2;
public reportDataGlitchMail: string;
@ -99,15 +97,13 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
dividendInBaseCurrency,
feeInBaseCurrency,
firstBuyDate,
grossPerformance,
grossPerformancePercent,
historicalData,
investment,
marketPrice,
maxPrice,
minPrice,
netPerformance,
netPerformancePercent,
netPerformancePercentWithCurrencyEffect,
netPerformanceWithCurrencyEffect,
orders,
quantity,
SymbolProfile,
@ -125,8 +121,6 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.dividendInBaseCurrency = dividendInBaseCurrency;
this.feeInBaseCurrency = feeInBaseCurrency;
this.firstBuyDate = firstBuyDate;
this.grossPerformance = grossPerformance;
this.grossPerformancePercent = grossPerformancePercent;
this.historicalDataItems = historicalData.map(
(historicalDataItem) => {
this.benchmarkDataItems.push({
@ -144,8 +138,10 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.marketPrice = marketPrice;
this.maxPrice = maxPrice;
this.minPrice = minPrice;
this.netPerformance = netPerformance;
this.netPerformancePercent = netPerformancePercent;
this.netPerformancePercentWithCurrencyEffect =
netPerformancePercentWithCurrencyEffect;
this.netPerformanceWithCurrencyEffect =
netPerformanceWithCurrencyEffect;
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.sectors = {};

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

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

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

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

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

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

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

Loading…
Cancel
Save