Browse Source

Task/move percentage formulas of portfolio calculator to method class (#7919)

Move percentage formulas of portfolio calculator to method class
pull/7922/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
89da484bb7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 16
      apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
  2. 146
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 2
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts
  4. 6
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts
  5. 207
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  6. 16
      apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts
  7. 16
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts
  8. 15
      apps/api/src/app/portfolio/interfaces/accumulated-values.interface.ts
  9. 2
      apps/api/src/app/portfolio/interfaces/holding-performance.interface.ts
  10. 45
      apps/api/src/app/portfolio/portfolio.service.ts
  11. 9
      apps/api/src/app/portfolio/types/net-performance-percentages.type.ts
  12. 2
      libs/common/src/lib/interfaces/portfolio-summary.interface.ts
  13. 8
      libs/common/src/lib/models/portfolio-snapshot-holding.ts
  14. 8
      libs/common/src/lib/models/portfolio-snapshot.ts

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

@ -1,12 +1,24 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.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 { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export class MwrPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance([]: PortfolioCalculatorHolding[]): PortfolioSnapshot {
protected calculateNetPerformancePercentages(): {
[date: string]: NetPerformancePercentages;
} {
throw new Error('Method not implemented.');
}
protected calculateNetPerformancePercentagesForDateRange(): {
[date: string]: NetPerformancePercentages;
} {
throw new Error('Method not implemented.');
}
protected calculateOverallPerformance(): PortfolioSnapshot {
throw new Error('Method not implemented.');
}

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

@ -1,5 +1,6 @@
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { PortfolioSnapshotComputationError } from '@ghostfolio/api/app/portfolio/errors/portfolio-snapshot-computation.error';
import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.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 { HoldingValuation } from '@ghostfolio/api/app/portfolio/interfaces/holding-valuation.interface';
@ -9,6 +10,7 @@ import { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interf
import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.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 { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
@ -41,7 +43,10 @@ import {
InvestmentItem,
ResponseError
} from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models';
import {
PortfolioSnapshot,
PortfolioSnapshotHolding
} from '@ghostfolio/common/models';
import { GroupBy } from '@ghostfolio/common/types';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
@ -67,7 +72,7 @@ import {
startOfYear,
subDays
} from 'date-fns';
import { groupBy, isNumber, sortBy, sum, uniqBy } from 'lodash';
import { groupBy, sortBy, uniqBy } from 'lodash';
export abstract class PortfolioCalculator {
protected static readonly ENABLE_LOGGING = false;
@ -213,6 +218,18 @@ export abstract class PortfolioCalculator {
this.snapshotPromise.catch(() => undefined);
}
protected abstract calculateNetPerformancePercentages({
accumulatedValuesByDate
}: {
accumulatedValuesByDate: { [date: string]: AccumulatedValues };
}): { [date: string]: NetPerformancePercentages };
protected abstract calculateNetPerformancePercentagesForDateRange({
historicalDataItems
}: {
historicalDataItems: HistoricalDataItem[];
}): { [date: string]: NetPerformancePercentages };
protected abstract calculateOverallPerformance(
positions: PortfolioCalculatorHolding[]
): PortfolioSnapshot;
@ -230,6 +247,8 @@ export abstract class PortfolioCalculator {
activitiesCount: 0,
createdAt: new Date(),
currentValueInBaseCurrency: new Big(0),
dividendYieldPercent: new Big(0),
dividendYieldPercentWithCurrencyEffect: new Big(0),
errors: [],
hasErrors: false,
historicalData: [],
@ -353,19 +372,7 @@ export abstract class PortfolioCalculator {
const positions: PortfolioCalculatorHolding[] = [];
const accumulatedValuesByDate: {
[date: string]: {
investmentValueWithCurrencyEffect: Big;
totalAverageInvestmentValue: Big;
totalAverageInvestmentValueWithCurrencyEffect: Big;
totalCashValueWithCurrencyEffect: Big;
totalCurrentValue: Big;
totalCurrentValueWithCurrencyEffect: Big;
totalInvestmentValue: Big;
totalInvestmentValueWithCurrencyEffect: Big;
totalNetPerformanceValue: Big;
totalNetPerformanceValueWithCurrencyEffect: Big;
totalNetWorthValueWithCurrencyEffect: Big;
};
[date: string]: AccumulatedValues;
} = {};
const valuesByAssetProfileIdentifier: {
@ -409,6 +416,8 @@ export abstract class PortfolioCalculator {
averageInvestmentWithCurrencyEffect,
currentValues,
currentValuesWithCurrencyEffect,
dividendYieldPercent,
dividendYieldPercentWithCurrencyEffect,
grossPerformance,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
@ -477,6 +486,8 @@ export abstract class PortfolioCalculator {
positions.push({
averageInvestment,
averageInvestmentWithCurrencyEffect,
dividendYieldPercent,
dividendYieldPercentWithCurrencyEffect,
activitiesCount: item.activitiesCount,
averagePrice: item.averagePrice,
currency: item.currency,
@ -647,13 +658,14 @@ export abstract class PortfolioCalculator {
}
}
const netPerformancePercentagesByDate =
this.calculateNetPerformancePercentages({ accumulatedValuesByDate });
const historicalData: HistoricalDataItem[] = Object.entries(
accumulatedValuesByDate
).map(([date, values]) => {
const {
investmentValueWithCurrencyEffect,
totalAverageInvestmentValue,
totalAverageInvestmentValueWithCurrencyEffect,
totalCashValueWithCurrencyEffect,
totalCurrentValue,
totalCurrentValueWithCurrencyEffect,
@ -664,21 +676,9 @@ export abstract class PortfolioCalculator {
totalNetWorthValueWithCurrencyEffect
} = values;
const netPerformanceInPercentage = totalAverageInvestmentValue.eq(0)
? 0
: totalNetPerformanceValue.div(totalAverageInvestmentValue).toNumber();
const netPerformanceInPercentageWithCurrencyEffect =
totalAverageInvestmentValueWithCurrencyEffect.eq(0)
? 0
: totalNetPerformanceValueWithCurrencyEffect
.div(totalAverageInvestmentValueWithCurrencyEffect)
.toNumber();
return {
...netPerformancePercentagesByDate[date],
date,
netPerformanceInPercentage,
netPerformanceInPercentageWithCurrencyEffect,
investmentValueWithCurrencyEffect:
investmentValueWithCurrencyEffect.toNumber(),
netPerformance: totalNetPerformanceValue.toNumber(),
@ -857,8 +857,14 @@ export abstract class PortfolioCalculator {
public async getDividendInBaseCurrency() {
await this.snapshotPromise;
return this.getDividendInBaseCurrencyOfHoldings(this.snapshot.positions);
}
protected getDividendInBaseCurrencyOfHoldings(
holdings: PortfolioSnapshotHolding[]
) {
return getSum(
this.snapshot.positions.map(({ dividendInBaseCurrency }) => {
holdings.map(({ dividendInBaseCurrency }) => {
return dividendInBaseCurrency;
})
);
@ -872,6 +878,8 @@ export abstract class PortfolioCalculator {
averageInvestmentWithCurrencyEffect: new Big(0),
currentValues: {},
currentValuesWithCurrencyEffect: {},
dividendYieldPercent: new Big(0),
dividendYieldPercentWithCurrencyEffect: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -1342,12 +1350,8 @@ export abstract class PortfolioCalculator {
const { historicalData } = this.snapshot;
const chart: HistoricalDataItem[] = [];
const historicalDataItemsOfDateRange: HistoricalDataItem[] = [];
const averageInvestmentValues: number[] = [];
const averageInvestmentValuesWithCurrencyEffect: number[] = [];
let grossPerformanceAtStartDate: number;
let grossPerformanceWithCurrencyEffectAtStartDate: number;
let netPerformanceAtStartDate: number;
let netPerformanceWithCurrencyEffectAtStartDate: number;
@ -1355,71 +1359,37 @@ export abstract class PortfolioCalculator {
const date = resetHours(parseDate(historicalDataItem.date));
if (!isBefore(date, start) && !isAfter(date, end)) {
if (!isNumber(netPerformanceAtStartDate)) {
grossPerformanceAtStartDate =
historicalDataItem.value - historicalDataItem.totalInvestment;
grossPerformanceWithCurrencyEffectAtStartDate =
historicalDataItem.valueWithCurrencyEffect -
historicalDataItem.totalInvestmentValueWithCurrencyEffect;
// Take the values at the start date from the first day of the date
// range
if (historicalDataItemsOfDateRange.length === 0) {
netPerformanceAtStartDate = historicalDataItem.netPerformance;
netPerformanceWithCurrencyEffectAtStartDate =
historicalDataItem.netPerformanceWithCurrencyEffect;
}
const netPerformanceSinceStartDate =
historicalDataItem.netPerformance - netPerformanceAtStartDate;
const netPerformanceWithCurrencyEffectSinceStartDate =
historicalDataItemsOfDateRange.push({
...historicalDataItem,
netPerformance:
historicalDataItem.netPerformance - netPerformanceAtStartDate,
netPerformanceWithCurrencyEffect:
historicalDataItem.netPerformanceWithCurrencyEffect -
netPerformanceWithCurrencyEffectAtStartDate;
// Add the gross performance at the start date of the range to the
// investment of each day. Thus the range starts with the value of its
// first day, and subsequent buy and sell activities stay included.
if (historicalDataItem.totalInvestment > 0) {
averageInvestmentValues.push(
historicalDataItem.totalInvestment + grossPerformanceAtStartDate
);
netPerformanceWithCurrencyEffectAtStartDate
});
}
if (historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0) {
averageInvestmentValuesWithCurrencyEffect.push(
historicalDataItem.totalInvestmentValueWithCurrencyEffect +
grossPerformanceWithCurrencyEffectAtStartDate
);
}
const averageInvestmentValue =
averageInvestmentValues.length > 0
? sum(averageInvestmentValues) / averageInvestmentValues.length
: 0;
const averageInvestmentValueWithCurrencyEffect =
averageInvestmentValuesWithCurrencyEffect.length > 0
? sum(averageInvestmentValuesWithCurrencyEffect) /
averageInvestmentValuesWithCurrencyEffect.length
: 0;
const netPerformancePercentagesByDate =
this.calculateNetPerformancePercentagesForDateRange({
historicalDataItems: historicalDataItemsOfDateRange
});
chart.push({
const chart = historicalDataItemsOfDateRange.map((historicalDataItem) => {
return {
...historicalDataItem,
netPerformance: netPerformanceSinceStartDate,
netPerformanceWithCurrencyEffect:
netPerformanceWithCurrencyEffectSinceStartDate,
netPerformanceInPercentage:
averageInvestmentValue > 0
? netPerformanceSinceStartDate / averageInvestmentValue
: 0,
netPerformanceInPercentageWithCurrencyEffect:
averageInvestmentValueWithCurrencyEffect > 0
? netPerformanceWithCurrencyEffectSinceStartDate /
averageInvestmentValueWithCurrencyEffect
: 0
...netPerformancePercentagesByDate[historicalDataItem.date]
};
});
}
}
return { chart };
}

2
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts

@ -262,6 +262,8 @@ describe('PortfolioCalculator', () => {
dateOfFirstActivity: '2023-12-31',
dividend: new Big(0),
dividendInBaseCurrency: new Big(0),
dividendYieldPercent: new Big(0),
dividendYieldPercentWithCurrencyEffect: new Big(0),
fee: new Big(0),
feeInBaseCurrency: new Big(0),
grossPerformance: new Big(0),

6
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts

@ -137,6 +137,8 @@ describe('PortfolioCalculator', () => {
});
expect(portfolioSnapshot).toMatchObject({
dividendYieldPercent: new Big('0.001144362748184'),
dividendYieldPercentWithCurrencyEffect: new Big('0.001144362748184'),
errors: [],
hasErrors: false,
positions: [
@ -148,6 +150,10 @@ describe('PortfolioCalculator', () => {
dateOfFirstActivity: '2021-09-16',
dividend: new Big('0.62'),
dividendInBaseCurrency: new Big('0.62'),
dividendYieldPercent: new Big('0.001144362748184'),
dividendYieldPercentWithCurrencyEffect: new Big(
'0.001144362748184'
),
fee: new Big('19'),
grossPerformance: new Big('33.25'),
grossPerformancePercentage: new Big('0.11136043941322258691'),

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

@ -1,14 +1,22 @@
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 { 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 { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type';
import {
getAnnualizedPerformancePercent,
getIntervalFromDateRange
} from '@ghostfolio/common/calculation-helper';
import {
DATE_FORMAT,
getAssetProfileIdentifier,
parseDate
} from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import {
AssetProfileIdentifier,
HistoricalDataItem
} from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models';
import { DateRange } from '@ghostfolio/common/types';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
@ -21,16 +29,119 @@ import {
isBefore,
isThisYear
} from 'date-fns';
import { sum } from 'lodash';
export class RoaiPortfolioCalculator extends PortfolioCalculator {
protected calculateNetPerformancePercentages({
accumulatedValuesByDate
}: {
accumulatedValuesByDate: { [date: string]: AccumulatedValues };
}): { [date: string]: NetPerformancePercentages } {
const netPerformancePercentagesByDate: {
[date: string]: NetPerformancePercentages;
} = {};
for (const [
date,
{
totalAverageInvestmentValue,
totalAverageInvestmentValueWithCurrencyEffect,
totalNetPerformanceValue,
totalNetPerformanceValueWithCurrencyEffect
}
] of Object.entries(accumulatedValuesByDate)) {
netPerformancePercentagesByDate[date] = {
netPerformanceInPercentage: totalAverageInvestmentValue.eq(0)
? 0
: totalNetPerformanceValue
.div(totalAverageInvestmentValue)
.toNumber(),
netPerformanceInPercentageWithCurrencyEffect:
totalAverageInvestmentValueWithCurrencyEffect.eq(0)
? 0
: totalNetPerformanceValueWithCurrencyEffect
.div(totalAverageInvestmentValueWithCurrencyEffect)
.toNumber()
};
}
return netPerformancePercentagesByDate;
}
protected calculateNetPerformancePercentagesForDateRange({
historicalDataItems
}: {
historicalDataItems: HistoricalDataItem[];
}): { [date: string]: NetPerformancePercentages } {
const averageInvestmentValues: number[] = [];
const averageInvestmentValuesWithCurrencyEffect: number[] = [];
let grossPerformanceAtStartDate: number;
let grossPerformanceWithCurrencyEffectAtStartDate: number;
const netPerformancePercentagesByDate: {
[date: string]: NetPerformancePercentages;
} = {};
for (const [index, historicalDataItem] of historicalDataItems.entries()) {
// Take the values at the start date from the first day of the date
// range
if (index === 0) {
grossPerformanceAtStartDate =
historicalDataItem.value - historicalDataItem.totalInvestment;
grossPerformanceWithCurrencyEffectAtStartDate =
historicalDataItem.valueWithCurrencyEffect -
historicalDataItem.totalInvestmentValueWithCurrencyEffect;
}
// Add the gross performance at the start date of the range to the
// investment of each day. Thus the range starts with the value of its
// first day, and subsequent buy and sell activities stay included.
if (historicalDataItem.totalInvestment > 0) {
averageInvestmentValues.push(
historicalDataItem.totalInvestment + grossPerformanceAtStartDate
);
}
if (historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0) {
averageInvestmentValuesWithCurrencyEffect.push(
historicalDataItem.totalInvestmentValueWithCurrencyEffect +
grossPerformanceWithCurrencyEffectAtStartDate
);
}
const averageInvestmentValue =
averageInvestmentValues.length > 0
? sum(averageInvestmentValues) / averageInvestmentValues.length
: 0;
const averageInvestmentValueWithCurrencyEffect =
averageInvestmentValuesWithCurrencyEffect.length > 0
? sum(averageInvestmentValuesWithCurrencyEffect) /
averageInvestmentValuesWithCurrencyEffect.length
: 0;
netPerformancePercentagesByDate[historicalDataItem.date] = {
netPerformanceInPercentage:
averageInvestmentValue > 0
? historicalDataItem.netPerformance / averageInvestmentValue
: 0,
netPerformanceInPercentageWithCurrencyEffect:
averageInvestmentValueWithCurrencyEffect > 0
? historicalDataItem.netPerformanceWithCurrencyEffect /
averageInvestmentValueWithCurrencyEffect
: 0
};
}
return netPerformancePercentagesByDate;
}
protected calculateOverallPerformance(
positions: PortfolioCalculatorHolding[]
): PortfolioSnapshot {
let currentValueInBaseCurrency = new Big(0);
let grossPerformance = new Big(0);
let grossPerformanceWithCurrencyEffect = new Big(0);
let hasErrors = false;
let netPerformance = new Big(0);
let totalAverageInvestment = new Big(0);
let totalAverageInvestmentWithCurrencyEffect = new Big(0);
let totalFeesWithCurrencyEffect = new Big(0);
@ -68,18 +179,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
hasErrors = true;
}
if (currentPosition.grossPerformance) {
grossPerformance = grossPerformance.plus(
currentPosition.grossPerformance
);
grossPerformanceWithCurrencyEffect =
grossPerformanceWithCurrencyEffect.plus(
currentPosition.grossPerformanceWithCurrencyEffect
);
netPerformance = netPerformance.plus(currentPosition.netPerformance);
} else if (!currentPosition.quantity.eq(0)) {
if (
!currentPosition.grossPerformance &&
!currentPosition.quantity.eq(0)
) {
hasErrors = true;
}
@ -101,8 +204,54 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}
}
const dateOfFirstActivity = this.getStartDate();
const daysInMarket = dateOfFirstActivity
? differenceInDays(new Date(), dateOfFirstActivity)
: 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({
daysInMarket,
netPerformancePercentage:
hasDividendWithoutAverageInvestment || totalAverageInvestment.eq(0)
? new Big(0)
: totalDividendInBaseCurrency.div(totalAverageInvestment)
});
const dividendYieldPercentWithCurrencyEffect =
getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercentage:
hasDividendWithoutAverageInvestment ||
totalAverageInvestmentWithCurrencyEffect.eq(0)
? new Big(0)
: totalDividendInBaseCurrency.div(
totalAverageInvestmentWithCurrencyEffect
)
});
return {
currentValueInBaseCurrency,
dividendYieldPercent,
dividendYieldPercentWithCurrencyEffect,
hasErrors,
positions,
totalFeesWithCurrencyEffect,
@ -401,6 +550,28 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
? totalNetPerformance.div(averageInvestmentBetweenStartAndEndDate)
: new Big(0);
const daysInMarket = differenceInDays(new Date(), dateOfFirstActivity);
const dividendYieldPercent = getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercentage: averageInvestmentBetweenStartAndEndDate.eq(0)
? new Big(0)
: totalDividendInBaseCurrency.div(
averageInvestmentBetweenStartAndEndDate
)
});
const dividendYieldPercentWithCurrencyEffect =
getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercentage:
averageInvestmentBetweenStartAndEndDateWithCurrencyEffect.eq(0)
? new Big(0)
: totalDividendInBaseCurrency.div(
averageInvestmentBetweenStartAndEndDateWithCurrencyEffect
)
});
const netPerformancePercentageWithCurrencyEffectMap: {
[key: DateRange]: Big;
} = {};
@ -538,6 +709,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
averageInvestmentValuesWithCurrencyEffect,
currentValues,
currentValuesWithCurrencyEffect,
dividendYieldPercent,
dividendYieldPercentWithCurrencyEffect,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
investmentValuesAccumulated,

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

@ -1,12 +1,24 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.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 { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export class RoiPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance([]: PortfolioCalculatorHolding[]): PortfolioSnapshot {
protected calculateNetPerformancePercentages(): {
[date: string]: NetPerformancePercentages;
} {
throw new Error('Method not implemented.');
}
protected calculateNetPerformancePercentagesForDateRange(): {
[date: string]: NetPerformancePercentages;
} {
throw new Error('Method not implemented.');
}
protected calculateOverallPerformance(): PortfolioSnapshot {
throw new Error('Method not implemented.');
}

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

@ -1,12 +1,24 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.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 { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
export class TwrPortfolioCalculator extends PortfolioCalculator {
protected calculateOverallPerformance([]: PortfolioCalculatorHolding[]): PortfolioSnapshot {
protected calculateNetPerformancePercentages(): {
[date: string]: NetPerformancePercentages;
} {
throw new Error('Method not implemented.');
}
protected calculateNetPerformancePercentagesForDateRange(): {
[date: string]: NetPerformancePercentages;
} {
throw new Error('Method not implemented.');
}
protected calculateOverallPerformance(): PortfolioSnapshot {
throw new Error('Method not implemented.');
}

15
apps/api/src/app/portfolio/interfaces/accumulated-values.interface.ts

@ -0,0 +1,15 @@
import { Big } from 'big.js';
export interface AccumulatedValues {
investmentValueWithCurrencyEffect: Big;
totalAverageInvestmentValue: Big;
totalAverageInvestmentValueWithCurrencyEffect: Big;
totalCashValueWithCurrencyEffect: Big;
totalCurrentValue: Big;
totalCurrentValueWithCurrencyEffect: Big;
totalInvestmentValue: Big;
totalInvestmentValueWithCurrencyEffect: Big;
totalNetPerformanceValue: Big;
totalNetPerformanceValueWithCurrencyEffect: Big;
totalNetWorthValueWithCurrencyEffect: Big;
}

2
apps/api/src/app/portfolio/interfaces/holding-performance.interface.ts

@ -17,6 +17,8 @@ export interface HoldingPerformance {
currentValuesWithCurrencyEffect: {
[date: string]: Big;
};
dividendYieldPercent: Big;
dividendYieldPercentWithCurrencyEffect: Big;
grossPerformance: Big;
grossPerformancePercentage: Big;
grossPerformancePercentageWithCurrencyEffect: Big;

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

@ -990,6 +990,9 @@ export class PortfolioService {
currency,
dateOfFirstActivity,
dividendInBaseCurrency,
dividendYieldPercent: dividendYieldPercentOfSnapshot,
dividendYieldPercentWithCurrencyEffect:
dividendYieldPercentWithCurrencyEffectOfSnapshot,
feeInBaseCurrency,
grossPerformance,
grossPerformancePercentage,
@ -1005,34 +1008,41 @@ export class PortfolioService {
tags
} = holding;
const activitiesOfHolding = activities.filter((activity) => {
return (
activity.assetProfile.dataSource === dataSource &&
activity.assetProfile.symbol === symbol
);
});
const dividendYieldPercent = getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(
// 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: differenceInDays(
new Date(),
parseDate(dateOfFirstActivity)
),
daysInMarket,
netPerformancePercentage: averageInvestmentWithCurrencyEffect.eq(0)
? new Big(0)
: dividendInBaseCurrency.div(averageInvestmentWithCurrencyEffect)
});
const activitiesOfHolding = activities.filter((activity) => {
return (
activity.assetProfile.dataSource === dataSource &&
activity.assetProfile.symbol === symbol
);
});
const historicalData = await this.dataProviderService.getHistorical(
[{ dataSource, symbol }],
'day',
@ -2097,6 +2107,8 @@ export class PortfolioService {
}
const {
dividendYieldPercent,
dividendYieldPercentWithCurrencyEffect,
totalCashInBaseCurrency,
totalInvestment,
totalInvestmentWithCurrencyEffect,
@ -2219,6 +2231,11 @@ export class PortfolioService {
return ['BUY', 'SELL'].includes(type);
}).length,
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
// TODO: Remove the fallback to 0 with the next release, when each
// cached portfolio snapshot contains the dividend yield
dividendYieldPercent: dividendYieldPercent?.toNumber() ?? 0,
dividendYieldPercentWithCurrencyEffect:
dividendYieldPercentWithCurrencyEffect?.toNumber() ?? 0,
emergencyFund: {
assets: emergencyFundHoldingsValueInBaseCurrency,
cash: totalEmergencyFund

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'
>
>;

2
libs/common/src/lib/interfaces/portfolio-summary.interface.ts

@ -8,6 +8,8 @@ export interface PortfolioSummary extends PortfolioPerformance {
cash: number;
dateOfFirstActivity?: Date;
dividendInBaseCurrency: number;
dividendYieldPercent: number;
dividendYieldPercentWithCurrencyEffect: number;
emergencyFund: {
assets: number;
cash: number;

8
libs/common/src/lib/models/portfolio-snapshot-holding.ts

@ -60,6 +60,14 @@ export class PortfolioSnapshotHolding {
@Type(() => Big)
dividendInBaseCurrency: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
dividendYieldPercent: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
dividendYieldPercentWithCurrencyEffect: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
fee: Big;

8
libs/common/src/lib/models/portfolio-snapshot.ts

@ -17,6 +17,14 @@ export class PortfolioSnapshot {
@Type(() => Big)
currentValueInBaseCurrency: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
dividendYieldPercent: Big;
@Transform(transformToBig, { toClassOnly: true })
@Type(() => Big)
dividendYieldPercentWithCurrencyEffect: Big;
errors: AssetProfileIdentifier[];
hasErrors: boolean;

Loading…
Cancel
Save