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
This commit is contained in:
parent
c002e37285
commit
2e9d40c201
@ -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…
x
Reference in New Issue
Block a user