net performance for current positions (#330)

* implement fees for transaction points #324

* add net performance to current positions #324

* add net performance to calculate timeline #324

* make timeline fee accumulated by default #324

* Update changelog

Co-authored-by: Valentin Zickner <github@zickner.ch>
Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
pull/347/head
Valentin Zickner 3 years ago committed by GitHub
parent ba234a470e
commit 48ab862bb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added a story for the trend indicator component
- Added a story for the value component
### Changed
- Switched from gross to net performance
- Restructured the portfolio summary tab on the home page (fees and net performance)
## 1.45.0 - 04.09.2021
### Added

@ -6,6 +6,8 @@ export interface CurrentPositions {
positions: TimelinePosition[];
grossPerformance: Big;
grossPerformancePercentage: Big;
netPerformance: Big;
netPerformancePercentage: Big;
currentValue: Big;
totalInvestment: Big;
}

@ -5,6 +5,7 @@ import Big from 'big.js';
export interface PortfolioOrder {
currency: Currency;
date: string;
fee: Big;
name: string;
quantity: Big;
symbol: string;

@ -11,6 +11,8 @@ export interface PortfolioPositionDetail {
marketPrice: number;
maxPrice: number;
minPrice: number;
netPerformance: number;
netPerformancePercent: number;
quantity: number;
symbol: string;
transactionCount: number;

@ -4,5 +4,6 @@ export interface TimelinePeriod {
date: string;
grossPerformance: Big;
investment: Big;
netPerformance: Big;
value: Big;
}

@ -3,6 +3,7 @@ import Big from 'big.js';
export interface TransactionPointSymbol {
currency: Currency;
fee: Big;
firstBuyDate: string;
investment: Big;
quantity: Big;

File diff suppressed because it is too large Load Diff

@ -58,6 +58,7 @@ export class PortfolioCalculator {
.plus(oldAccumulatedSymbol.quantity);
currentTransactionPointItem = {
currency: order.currency,
fee: order.fee.plus(oldAccumulatedSymbol.fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
investment: newQuantity.eq(0)
? new Big(0)
@ -72,6 +73,7 @@ export class PortfolioCalculator {
} else {
currentTransactionPointItem = {
currency: order.currency,
fee: order.fee,
firstBuyDate: order.date,
investment: unitPrice.mul(order.quantity).mul(factor),
quantity: order.quantity.mul(factor),
@ -112,11 +114,13 @@ export class PortfolioCalculator {
public async getCurrentPositions(start: Date): Promise<CurrentPositions> {
if (!this.transactionPoints?.length) {
return {
currentValue: new Big(0),
hasErrors: false,
positions: [],
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
currentValue: new Big(0),
netPerformance: new Big(0),
netPerformancePercentage: new Big(0),
positions: [],
totalInvestment: new Big(0)
};
}
@ -181,7 +185,9 @@ export class PortfolioCalculator {
const startString = format(start, DATE_FORMAT);
const holdingPeriodReturns: { [symbol: string]: Big } = {};
const netHoldingPeriodReturns: { [symbol: string]: Big } = {};
const grossPerformance: { [symbol: string]: Big } = {};
const netPerformance: { [symbol: string]: Big } = {};
const todayString = format(today, DATE_FORMAT);
if (firstIndex > 0) {
@ -190,6 +196,7 @@ export class PortfolioCalculator {
const invalidSymbols = [];
const lastInvestments: { [symbol: string]: Big } = {};
const lastQuantities: { [symbol: string]: Big } = {};
const lastFees: { [symbol: string]: Big } = {};
const initialValues: { [symbol: string]: Big } = {};
for (let i = firstIndex; i < this.transactionPoints.length; i++) {
@ -202,10 +209,6 @@ export class PortfolioCalculator {
const items = this.transactionPoints[i].items;
for (const item of items) {
let oldHoldingPeriodReturn = holdingPeriodReturns[item.symbol];
if (!oldHoldingPeriodReturn) {
oldHoldingPeriodReturn = new Big(1);
}
if (!marketSymbolMap[nextDate]?.[item.symbol]) {
invalidSymbols.push(item.symbol);
hasErrors = true;
@ -224,6 +227,13 @@ export class PortfolioCalculator {
const itemValue = marketSymbolMap[currentDate]?.[item.symbol];
let initialValue = itemValue?.mul(lastQuantity);
let investedValue = itemValue?.mul(item.quantity);
const isFirstOrderAndIsStartBeforeCurrentDate =
i === firstIndex &&
isBefore(parseDate(this.transactionPoints[i].date), start);
const lastFee: Big = lastFees[item.symbol] ?? new Big(0);
const fee = isFirstOrderAndIsStartBeforeCurrentDate
? new Big(0)
: item.fee.minus(lastFee);
if (!isAfter(parseDate(currentDate), parseDate(item.firstBuyDate))) {
initialValue = item.investment;
investedValue = item.investment;
@ -247,18 +257,26 @@ export class PortfolioCalculator {
);
const holdingPeriodReturn = endValue.div(initialValue.plus(cashFlow));
holdingPeriodReturns[item.symbol] =
oldHoldingPeriodReturn.mul(holdingPeriodReturn);
let oldGrossPerformance = grossPerformance[item.symbol];
if (!oldGrossPerformance) {
oldGrossPerformance = new Big(0);
}
const currentPerformance = endValue.minus(investedValue);
grossPerformance[item.symbol] =
oldGrossPerformance.plus(currentPerformance);
holdingPeriodReturns[item.symbol] = (
holdingPeriodReturns[item.symbol] ?? new Big(1)
).mul(holdingPeriodReturn);
grossPerformance[item.symbol] = (
grossPerformance[item.symbol] ?? new Big(0)
).plus(endValue.minus(investedValue));
const netHoldingPeriodReturn = endValue.div(
initialValue.plus(cashFlow).plus(fee)
);
netHoldingPeriodReturns[item.symbol] = (
netHoldingPeriodReturns[item.symbol] ?? new Big(1)
).mul(netHoldingPeriodReturn);
netPerformance[item.symbol] = (
netPerformance[item.symbol] ?? new Big(0)
).plus(endValue.minus(investedValue).minus(fee));
}
lastInvestments[item.symbol] = item.investment;
lastQuantities[item.symbol] = item.quantity;
lastFees[item.symbol] = item.fee;
}
}
@ -282,15 +300,17 @@ export class PortfolioCalculator {
: null,
investment: item.investment,
marketPrice: marketValue?.toNumber() ?? null,
netPerformance: isValid ? netPerformance[item.symbol] ?? null : null,
netPerformancePercentage:
isValid && netHoldingPeriodReturns[item.symbol]
? netHoldingPeriodReturns[item.symbol].minus(1)
: null,
quantity: item.quantity,
symbol: item.symbol,
transactionCount: item.transactionCount
});
}
const overall = this.calculateOverallGrossPerformance(
positions,
initialValues
);
const overall = this.calculateOverallPerformance(positions, initialValues);
return {
...overall,
@ -378,7 +398,7 @@ export class PortfolioCalculator {
return flatten(timelinePeriods);
}
private calculateOverallGrossPerformance(
private calculateOverallPerformance(
positions: TimelinePosition[],
initialValues: { [p: string]: Big }
) {
@ -387,6 +407,8 @@ export class PortfolioCalculator {
let totalInvestment = new Big(0);
let grossPerformance = new Big(0);
let grossPerformancePercentage = new Big(0);
let netPerformance = new Big(0);
let netPerformancePercentage = new Big(0);
let completeInitialValue = new Big(0);
for (const currentPosition of positions) {
if (currentPosition.marketPrice) {
@ -401,6 +423,7 @@ export class PortfolioCalculator {
grossPerformance = grossPerformance.plus(
currentPosition.grossPerformance
);
netPerformance = netPerformance.plus(currentPosition.netPerformance);
} else if (!currentPosition.quantity.eq(0)) {
hasErrors = true;
}
@ -414,6 +437,9 @@ export class PortfolioCalculator {
grossPerformancePercentage = grossPerformancePercentage.plus(
currentPosition.grossPerformancePercentage.mul(currentInitialValue)
);
netPerformancePercentage = netPerformancePercentage.plus(
currentPosition.netPerformancePercentage.mul(currentInitialValue)
);
} else if (!currentPosition.quantity.eq(0)) {
console.error(
`Initial value is missing for symbol ${currentPosition.symbol}`
@ -425,6 +451,8 @@ export class PortfolioCalculator {
if (!completeInitialValue.eq(0)) {
grossPerformancePercentage =
grossPerformancePercentage.div(completeInitialValue);
netPerformancePercentage =
netPerformancePercentage.div(completeInitialValue);
}
return {
@ -432,6 +460,8 @@ export class PortfolioCalculator {
grossPerformance,
grossPerformancePercentage,
hasErrors,
netPerformance,
netPerformancePercentage,
totalInvestment
};
}
@ -442,6 +472,7 @@ export class PortfolioCalculator {
endDate: Date
): Promise<TimelinePeriod[]> {
let investment: Big = new Big(0);
let fees: Big = new Big(0);
const marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
@ -454,6 +485,7 @@ export class PortfolioCalculator {
currencies[item.symbol] = item.currency;
symbols.push(item.symbol);
investment = investment.add(item.investment);
fees = fees.add(item.fee);
}
let marketSymbols: GetValueObject[] = [];
@ -490,7 +522,7 @@ export class PortfolioCalculator {
}
}
const results = [];
const results: TimelinePeriod[] = [];
for (
let currentDate = startDate;
isBefore(currentDate, endDate);
@ -513,11 +545,13 @@ export class PortfolioCalculator {
}
}
if (!invalid) {
const grossPerformance = value.minus(investment);
const result = {
date: currentDateAsString,
grossPerformance: value.minus(investment),
grossPerformance,
investment,
value
value,
date: currentDateAsString,
netPerformance: grossPerformance.minus(fees)
};
results.push(result);
}

@ -147,7 +147,7 @@ export class PortfolioService {
.map((timelineItem) => ({
date: timelineItem.date,
marketPrice: timelineItem.value,
value: timelineItem.grossPerformance.toNumber()
value: timelineItem.netPerformance.toNumber()
}));
}
@ -233,6 +233,8 @@ export class PortfolioService {
marketPrice: item.marketPrice,
marketState: dataProviderResponse.marketState,
name: symbolProfile.name,
netPerformance: item.netPerformance?.toNumber() ?? 0,
netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0,
quantity: item.quantity.toNumber(),
sectors: symbolProfile.sectors,
symbol: item.symbol,
@ -280,6 +282,8 @@ export class PortfolioService {
marketPrice: undefined,
maxPrice: undefined,
minPrice: undefined,
netPerformance: undefined,
netPerformancePercent: undefined,
quantity: undefined,
symbol: aSymbol,
transactionCount: undefined
@ -291,6 +295,7 @@ export class PortfolioService {
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
currency: order.currency,
date: format(order.date, DATE_FORMAT),
fee: new Big(order.fee),
name: order.SymbolProfile?.name,
quantity: new Big(order.quantity),
symbol: order.symbol,
@ -324,7 +329,7 @@ export class PortfolioService {
transactionCount
} = position;
// Convert investment and gross performance to currency of user
// Convert investment, gross and net performance to currency of user
const userCurrency = this.request.user.Settings.currency;
const investment = this.exchangeRateDataService.toCurrency(
position.investment.toNumber(),
@ -336,6 +341,11 @@ export class PortfolioService {
currency,
userCurrency
);
const netPerformance = this.exchangeRateDataService.toCurrency(
position.netPerformance.toNumber(),
currency,
userCurrency
);
const historicalData = await this.dataProviderService.getHistorical(
[aSymbol],
@ -397,10 +407,12 @@ export class PortfolioService {
marketPrice,
maxPrice,
minPrice,
netPerformance,
transactionCount,
averagePrice: averagePrice.toNumber(),
grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
historicalData: historicalDataArray,
netPerformancePercent: position.netPerformancePercentage.toNumber(),
quantity: quantity.toNumber(),
symbol: aSymbol
};
@ -450,6 +462,8 @@ export class PortfolioService {
grossPerformancePercent: undefined,
historicalData: historicalDataArray,
investment: 0,
netPerformance: undefined,
netPerformancePercent: undefined,
quantity: 0,
symbol: aSymbol,
transactionCount: undefined
@ -513,6 +527,9 @@ export class PortfolioService {
investment: new Big(position.investment).toNumber(),
marketState: dataProviderResponses[position.symbol].marketState,
name: symbolProfileMap[position.symbol].name,
netPerformance: position.netPerformance?.toNumber() ?? null,
netPerformancePercentage:
position.netPerformancePercentage?.toNumber() ?? null,
quantity: new Big(position.quantity).toNumber()
};
})
@ -538,6 +555,8 @@ export class PortfolioService {
performance: {
currentGrossPerformance: 0,
currentGrossPerformancePercent: 0,
currentNetPerformance: 0,
currentNetPerformancePercent: 0,
currentValue: 0
}
};
@ -557,11 +576,17 @@ export class PortfolioService {
currentPositions.grossPerformance.toNumber();
const currentGrossPerformancePercent =
currentPositions.grossPerformancePercentage.toNumber();
const currentNetPerformance = currentPositions.netPerformance.toNumber();
const currentNetPerformancePercent =
currentPositions.netPerformancePercentage.toNumber();
return {
hasErrors: currentPositions.hasErrors || hasErrors,
performance: {
currentGrossPerformance,
currentGrossPerformancePercent,
currentNetPerformance,
currentNetPerformancePercent,
currentValue: currentValue
}
};
@ -732,6 +757,8 @@ export class PortfolioService {
marketPrice: 0,
marketState: MarketState.open,
name: 'Cash',
netPerformance: 0,
netPerformancePercent: 0,
quantity: 0,
sectors: [],
symbol: ghostfolioCashSymbol,
@ -778,6 +805,13 @@ export class PortfolioService {
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
currency: order.currency,
date: format(order.date, DATE_FORMAT),
fee: new Big(
this.exchangeRateDataService.toCurrency(
order.fee,
order.currency,
userCurrency
)
),
name: order.SymbolProfile?.name,
quantity: new Big(order.quantity),
symbol: order.symbol,

@ -37,7 +37,7 @@
[colorizeSign]="true"
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : performance?.currentGrossPerformance"
[value]="isLoading ? undefined : performance?.currentNetPerformance"
></gf-value>
</div>
<div class="col">
@ -46,7 +46,7 @@
[isPercent]="true"
[locale]="locale"
[value]="
isLoading ? undefined : performance?.currentGrossPerformancePercent
isLoading ? undefined : performance?.currentNetPerformancePercent
"
></gf-value>
</div>

@ -52,7 +52,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
new CountUp(
'value',
this.performance?.currentGrossPerformancePercent * 100,
this.performance?.currentNetPerformancePercent * 100,
{
decimalPlaces: 2,
duration: 0.75,

@ -9,23 +9,6 @@
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3">
<div class="d-flex flex-grow-1" i18n>
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
{order} other {orders}}
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.fees"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Buy</div>
<div class="d-flex justify-content-end">
@ -66,7 +49,7 @@
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Absolute Performance</div>
<div class="d-flex flex-grow-1" i18n>Absolute Gross Performance</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
@ -77,7 +60,7 @@
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1 ml-3" i18n>Performance (TWR)</div>
<div class="d-flex flex-grow-1 ml-3" i18n>Gross Performance (TWR)</div>
<div class="d-flex flex-column flex-wrap justify-content-end">
<gf-value
class="justify-content-end"
@ -91,6 +74,48 @@
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>
Fees for {{ summary?.ordersCount }} {summary?.ordersCount, plural, =1
{order} other {orders}}
</div>
<div class="d-flex justify-content-end">
<span *ngIf="summary?.fees || summary?.fees === 0" class="mr-1">-</span>
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.fees"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Absolute Net Performance</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : summary?.currentNetPerformance"
></gf-value>
</div>
</div>
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1 ml-3" i18n>Net Performance (TWR)</div>
<div class="d-flex flex-column flex-wrap justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : summary?.currentNetPerformancePercent"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>

@ -34,6 +34,8 @@ export class PositionDetailDialog implements OnDestroy {
public marketPrice: number;
public maxPrice: number;
public minPrice: number;
public netPerformance: number;
public netPerformancePercent: number;
public quantity: number;
public transactionCount: number;
@ -60,6 +62,8 @@ export class PositionDetailDialog implements OnDestroy {
marketPrice,
maxPrice,
minPrice,
netPerformance,
netPerformancePercent,
quantity,
transactionCount
}) => {
@ -86,6 +90,8 @@ export class PositionDetailDialog implements OnDestroy {
this.marketPrice = marketPrice;
this.maxPrice = maxPrice;
this.minPrice = minPrice;
this.netPerformance = netPerformance;
this.netPerformancePercent = netPerformancePercent;
this.quantity = quantity;
this.transactionCount = transactionCount;

@ -25,7 +25,7 @@
[colorizeSign]="true"
[currency]="data.baseCurrency"
[locale]="data.locale"
[value]="grossPerformance"
[value]="netPerformance"
></gf-value>
</div>
<div class="col-6 mb-3">
@ -35,7 +35,7 @@
[colorizeSign]="true"
[isPercent]="true"
[locale]="data.locale"
[value]="grossPerformancePercent"
[value]="netPerformancePercent"
></gf-value>
</div>
<div class="col-6 mb-3">

@ -11,7 +11,7 @@
[isLoading]="isLoading"
[marketState]="position?.marketState"
[range]="range"
[value]="position?.grossPerformancePercentage"
[value]="position?.netPerformancePercentage"
></gf-trend-indicator>
</div>
<div *ngIf="isLoading" class="flex-grow-1">
@ -47,13 +47,13 @@
[colorizeSign]="true"
[currency]="baseCurrency"
[locale]="locale"
[value]="position?.grossPerformance"
[value]="position?.netPerformance"
></gf-value>
<gf-value
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="position?.grossPerformancePercentage"
[value]="position?.netPerformancePercentage"
></gf-value>
</div>
</div>

@ -30,7 +30,7 @@
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.grossPerformancePercent"
[value]="isLoading ? undefined : element.netPerformancePercent"
></gf-value>
</div>
</td>

@ -1,5 +1,7 @@
export interface PortfolioPerformance {
currentGrossPerformance: number;
currentGrossPerformancePercent: number;
currentNetPerformance: number;
currentNetPerformancePercent: number;
currentValue: number;
}

@ -20,6 +20,8 @@ export interface PortfolioPosition {
marketPrice: number;
marketState: MarketState;
name: string;
netPerformance: number;
netPerformancePercent: number;
quantity: number;
sectors: Sector[];
transactionCount: number;

@ -13,6 +13,8 @@ export interface Position {
marketPrice?: number;
marketState?: MarketState;
name?: string;
netPerformance?: number;
netPerformancePercentage?: number;
quantity: number;
symbol: string;
transactionCount: number;

@ -9,6 +9,8 @@ export interface TimelinePosition {
grossPerformancePercentage: Big;
investment: Big;
marketPrice: number;
netPerformance: Big;
netPerformancePercentage: Big;
quantity: Big;
symbol: string;
transactionCount: number;

Loading…
Cancel
Save