Browse Source

Move percentage formulas of portfolio calculator to method class

pull/7919/head
Thomas Kaul 4 weeks ago
parent
commit
3eaa038532
  1. 20
      apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
  2. 21
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 54
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  4. 20
      apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts
  5. 20
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
  6. 6
      apps/api/src/app/portfolio/interfaces/net-performance-percentages.interface.ts
  7. 45
      apps/api/src/app/portfolio/portfolio.service.ts
  8. 9
      apps/api/src/app/portfolio/types/net-performance-percentages.type.ts

20
apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts

@ -1,24 +1,20 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface';
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface'; import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import { import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
AssetProfileIdentifier,
HistoricalDataItem
} from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models'; import { PortfolioSnapshot } from '@ghostfolio/common/models';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export class MwrPortfolioCalculator extends PortfolioCalculator { export class MwrPortfolioCalculator extends PortfolioCalculator {
protected calculateNetPerformancePercentages({}: { protected calculateNetPerformancePercentages(): {
accumulatedValuesByDate: { [date: string]: AccumulatedValues }; [date: string]: NetPerformancePercentages;
}): { [date: string]: NetPerformancePercentages } { } {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
protected calculateNetPerformancePercentagesForDateRange({}: { protected calculateNetPerformancePercentagesForDateRange(): {
historicalDataItems: HistoricalDataItem[]; [date: string]: NetPerformancePercentages;
}): { [date: string]: NetPerformancePercentages } { } {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }

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

@ -4,13 +4,13 @@ import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accu
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface';
import { HoldingValuationItem } from '@ghostfolio/api/app/portfolio/interfaces/holding-valuation-item.interface'; import { HoldingValuationItem } from '@ghostfolio/api/app/portfolio/interfaces/holding-valuation-item.interface';
import { HoldingValuation } from '@ghostfolio/api/app/portfolio/interfaces/holding-valuation.interface'; import { HoldingValuation } from '@ghostfolio/api/app/portfolio/interfaces/holding-valuation.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface';
import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity-item.interface'; import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity-item.interface';
import { PortfolioCalculatorActivity } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity.interface'; import { PortfolioCalculatorActivity } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity.interface';
import { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface'; import { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface';
import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface'; import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface';
import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
@ -43,7 +43,10 @@ import {
InvestmentItem, InvestmentItem,
ResponseError ResponseError
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models'; import {
PortfolioSnapshot,
PortfolioSnapshotHolding
} from '@ghostfolio/common/models';
import { GroupBy } from '@ghostfolio/common/types'; import { GroupBy } from '@ghostfolio/common/types';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
@ -69,7 +72,7 @@ import {
startOfYear, startOfYear,
subDays subDays
} from 'date-fns'; } from 'date-fns';
import { groupBy, isNumber, sortBy, uniqBy } from 'lodash'; import { groupBy, sortBy, uniqBy } from 'lodash';
export abstract class PortfolioCalculator { export abstract class PortfolioCalculator {
protected static readonly ENABLE_LOGGING = false; protected static readonly ENABLE_LOGGING = false;
@ -854,8 +857,14 @@ export abstract class PortfolioCalculator {
public async getDividendInBaseCurrency() { public async getDividendInBaseCurrency() {
await this.snapshotPromise; await this.snapshotPromise;
return this.getDividendInBaseCurrencyOfHoldings(this.snapshot.positions);
}
protected getDividendInBaseCurrencyOfHoldings(
holdings: PortfolioSnapshotHolding[]
) {
return getSum( return getSum(
this.snapshot.positions.map(({ dividendInBaseCurrency }) => { holdings.map(({ dividendInBaseCurrency }) => {
return dividendInBaseCurrency; return dividendInBaseCurrency;
}) })
); );
@ -1350,7 +1359,9 @@ export abstract class PortfolioCalculator {
const date = resetHours(parseDate(historicalDataItem.date)); const date = resetHours(parseDate(historicalDataItem.date));
if (!isBefore(date, start) && !isAfter(date, end)) { if (!isBefore(date, start) && !isAfter(date, end)) {
if (!isNumber(netPerformanceAtStartDate)) { // Take the values at the start date from the first day of the date
// range
if (historicalDataItemsOfDateRange.length === 0) {
netPerformanceAtStartDate = historicalDataItem.netPerformance; netPerformanceAtStartDate = historicalDataItem.netPerformance;
netPerformanceWithCurrencyEffectAtStartDate = netPerformanceWithCurrencyEffectAtStartDate =

54
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts

@ -1,9 +1,9 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface'; import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface';
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface';
import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity-item.interface'; import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity-item.interface';
import { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface'; import { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import { import {
getAnnualizedPerformancePercent, getAnnualizedPerformancePercent,
getIntervalFromDateRange getIntervalFromDateRange
@ -29,7 +29,7 @@ import {
isBefore, isBefore,
isThisYear isThisYear
} from 'date-fns'; } from 'date-fns';
import { isNumber, sum } from 'lodash'; import { sum } from 'lodash';
export class RoaiPortfolioCalculator extends PortfolioCalculator { export class RoaiPortfolioCalculator extends PortfolioCalculator {
protected calculateNetPerformancePercentages({ protected calculateNetPerformancePercentages({
@ -82,8 +82,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
[date: string]: NetPerformancePercentages; [date: string]: NetPerformancePercentages;
} = {}; } = {};
for (const historicalDataItem of historicalDataItems) { for (const [index, historicalDataItem] of historicalDataItems.entries()) {
if (!isNumber(grossPerformanceAtStartDate)) { // Take the values at the start date from the first day of the date
// range
if (index === 0) {
grossPerformanceAtStartDate = grossPerformanceAtStartDate =
historicalDataItem.value - historicalDataItem.totalInvestment; historicalDataItem.value - historicalDataItem.totalInvestment;
@ -142,7 +144,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let hasErrors = false; let hasErrors = false;
let totalAverageInvestment = new Big(0); let totalAverageInvestment = new Big(0);
let totalAverageInvestmentWithCurrencyEffect = new Big(0); let totalAverageInvestmentWithCurrencyEffect = new Big(0);
let totalDividendInBaseCurrency = new Big(0);
let totalFeesWithCurrencyEffect = new Big(0); let totalFeesWithCurrencyEffect = new Big(0);
const totalInterestWithCurrencyEffect = new Big(0); const totalInterestWithCurrencyEffect = new Big(0);
let totalInvestment = new Big(0); let totalInvestment = new Big(0);
@ -161,12 +162,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
continue; continue;
} }
if (currentPosition.dividendInBaseCurrency) {
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
currentPosition.dividendInBaseCurrency
);
}
if (currentPosition.feeInBaseCurrency) { if (currentPosition.feeInBaseCurrency) {
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus(
currentPosition.feeInBaseCurrency currentPosition.feeInBaseCurrency
@ -215,21 +210,42 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
? differenceInDays(new Date(), dateOfFirstActivity) ? differenceInDays(new Date(), dateOfFirstActivity)
: 0; : 0;
// Take the dividend from the same source as the portfolio summary, so
// that the response shows one dividend only
const totalDividendInBaseCurrency =
this.getDividendInBaseCurrencyOfHoldings(positions);
// A holding without a market price, and a holding which is excluded from
// the performance, gives a dividend but no average investment. Such a
// holding makes the dividend yield too high. Therefore the dividend yield
// stays 0 in this case.
const hasDividendWithoutAverageInvestment = positions.some(
({ averageInvestment, dividendInBaseCurrency, includeInPerformance }) => {
return (
!dividendInBaseCurrency.eq(0) &&
(!includeInPerformance || averageInvestment.eq(0))
);
}
);
const dividendYieldPercent = getAnnualizedPerformancePercent({ const dividendYieldPercent = getAnnualizedPerformancePercent({
daysInMarket, daysInMarket,
netPerformancePercentage: totalAverageInvestment.eq(0) netPerformancePercentage:
? new Big(0) hasDividendWithoutAverageInvestment || totalAverageInvestment.eq(0)
: totalDividendInBaseCurrency.div(totalAverageInvestment) ? new Big(0)
: totalDividendInBaseCurrency.div(totalAverageInvestment)
}); });
const dividendYieldPercentWithCurrencyEffect = const dividendYieldPercentWithCurrencyEffect =
getAnnualizedPerformancePercent({ getAnnualizedPerformancePercent({
daysInMarket, daysInMarket,
netPerformancePercentage: totalAverageInvestmentWithCurrencyEffect.eq(0) netPerformancePercentage:
? new Big(0) hasDividendWithoutAverageInvestment ||
: totalDividendInBaseCurrency.div( totalAverageInvestmentWithCurrencyEffect.eq(0)
totalAverageInvestmentWithCurrencyEffect ? new Big(0)
) : totalDividendInBaseCurrency.div(
totalAverageInvestmentWithCurrencyEffect
)
}); });
return { return {

20
apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts

@ -1,24 +1,20 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface';
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface'; import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import { import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
AssetProfileIdentifier,
HistoricalDataItem
} from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models'; import { PortfolioSnapshot } from '@ghostfolio/common/models';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export class RoiPortfolioCalculator extends PortfolioCalculator { export class RoiPortfolioCalculator extends PortfolioCalculator {
protected calculateNetPerformancePercentages({}: { protected calculateNetPerformancePercentages(): {
accumulatedValuesByDate: { [date: string]: AccumulatedValues }; [date: string]: NetPerformancePercentages;
}): { [date: string]: NetPerformancePercentages } { } {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
protected calculateNetPerformancePercentagesForDateRange({}: { protected calculateNetPerformancePercentagesForDateRange(): {
historicalDataItems: HistoricalDataItem[]; [date: string]: NetPerformancePercentages;
}): { [date: string]: NetPerformancePercentages } { } {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }

20
apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

@ -1,24 +1,20 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface';
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface'; import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import { import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
AssetProfileIdentifier,
HistoricalDataItem
} from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models'; import { PortfolioSnapshot } from '@ghostfolio/common/models';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export class TwrPortfolioCalculator extends PortfolioCalculator { export class TwrPortfolioCalculator extends PortfolioCalculator {
protected calculateNetPerformancePercentages({}: { protected calculateNetPerformancePercentages(): {
accumulatedValuesByDate: { [date: string]: AccumulatedValues }; [date: string]: NetPerformancePercentages;
}): { [date: string]: NetPerformancePercentages } { } {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
protected calculateNetPerformancePercentagesForDateRange({}: { protected calculateNetPerformancePercentagesForDateRange(): {
historicalDataItems: HistoricalDataItem[]; [date: string]: NetPerformancePercentages;
}): { [date: string]: NetPerformancePercentages } { } {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }

6
apps/api/src/app/portfolio/interfaces/net-performance-percentages.interface.ts

@ -1,6 +0,0 @@
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
export interface NetPerformancePercentages extends Pick<
HistoricalDataItem,
'netPerformanceInPercentage' | 'netPerformanceInPercentageWithCurrencyEffect'
> {}

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

@ -984,12 +984,15 @@ export class PortfolioService {
const { const {
activitiesCount, activitiesCount,
averageInvestment,
averageInvestmentWithCurrencyEffect,
averagePrice, averagePrice,
currency, currency,
dateOfFirstActivity, dateOfFirstActivity,
dividendInBaseCurrency, dividendInBaseCurrency,
dividendYieldPercent, dividendYieldPercent: dividendYieldPercentOfSnapshot,
dividendYieldPercentWithCurrencyEffect, dividendYieldPercentWithCurrencyEffect:
dividendYieldPercentWithCurrencyEffectOfSnapshot,
feeInBaseCurrency, feeInBaseCurrency,
grossPerformance, grossPerformance,
grossPerformancePercentage, grossPerformancePercentage,
@ -1005,6 +1008,34 @@ export class PortfolioService {
tags tags
} = holding; } = holding;
// TODO: Remove the block below with the next release, when each cached
// portfolio snapshot contains the dividend yield. Then take
// dividendYieldPercent and dividendYieldPercentWithCurrencyEffect
// directly from the holding and remove averageInvestment and
// averageInvestmentWithCurrencyEffect from the properties above
const daysInMarket = differenceInDays(
new Date(),
parseDate(dateOfFirstActivity)
);
const dividendYieldPercent =
dividendYieldPercentOfSnapshot ??
getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercentage: averageInvestment.eq(0)
? new Big(0)
: dividendInBaseCurrency.div(averageInvestment)
});
const dividendYieldPercentWithCurrencyEffect =
dividendYieldPercentWithCurrencyEffectOfSnapshot ??
getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercentage: averageInvestmentWithCurrencyEffect.eq(0)
? new Big(0)
: dividendInBaseCurrency.div(averageInvestmentWithCurrencyEffect)
});
const activitiesOfHolding = activities.filter((activity) => { const activitiesOfHolding = activities.filter((activity) => {
return ( return (
activity.assetProfile.dataSource === dataSource && activity.assetProfile.dataSource === dataSource &&
@ -1117,11 +1148,9 @@ export class PortfolioService {
.getDataProvider(dataSource) .getDataProvider(dataSource)
.getDataProviderInfo(), .getDataProviderInfo(),
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
// TODO: Remove the fallback with the next release, when the cached dividendYieldPercent: dividendYieldPercent.toNumber(),
// portfolio snapshots have been recalculated
dividendYieldPercent: dividendYieldPercent?.toNumber() ?? 0,
dividendYieldPercentWithCurrencyEffect: dividendYieldPercentWithCurrencyEffect:
dividendYieldPercentWithCurrencyEffect?.toNumber() ?? 0, dividendYieldPercentWithCurrencyEffect.toNumber(),
feeInBaseCurrency: feeInBaseCurrency.toNumber(), feeInBaseCurrency: feeInBaseCurrency.toNumber(),
grossPerformance: grossPerformance?.toNumber(), grossPerformance: grossPerformance?.toNumber(),
grossPerformancePercent: grossPerformancePercentage?.toNumber(), grossPerformancePercent: grossPerformancePercentage?.toNumber(),
@ -2202,8 +2231,8 @@ export class PortfolioService {
return ['BUY', 'SELL'].includes(type); return ['BUY', 'SELL'].includes(type);
}).length, }).length,
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
// TODO: Remove the fallback with the next release, when the cached // TODO: Remove the fallback to 0 with the next release, when each
// portfolio snapshots have been recalculated // cached portfolio snapshot contains the dividend yield
dividendYieldPercent: dividendYieldPercent?.toNumber() ?? 0, dividendYieldPercent: dividendYieldPercent?.toNumber() ?? 0,
dividendYieldPercentWithCurrencyEffect: dividendYieldPercentWithCurrencyEffect:
dividendYieldPercentWithCurrencyEffect?.toNumber() ?? 0, dividendYieldPercentWithCurrencyEffect?.toNumber() ?? 0,

9
apps/api/src/app/portfolio/types/net-performance-percentages.type.ts

@ -0,0 +1,9 @@
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
export type NetPerformancePercentages = Required<
Pick<
HistoricalDataItem,
| 'netPerformanceInPercentage'
| 'netPerformanceInPercentageWithCurrencyEffect'
>
>;
Loading…
Cancel
Save