Browse Source

Move percentage formulas of portfolio calculator to method class

pull/7919/head
Thomas Kaul 4 weeks ago
parent
commit
b546ca64cc
  1. 22
      apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
  2. 133
      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. 191
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  6. 22
      apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts
  7. 22
      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. 6
      apps/api/src/app/portfolio/interfaces/net-performance-percentages.interface.ts
  11. 38
      apps/api/src/app/portfolio/portfolio.service.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

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

@ -1,12 +1,28 @@
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 { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface'; import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import {
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 calculateOverallPerformance([]: PortfolioCalculatorHolding[]): PortfolioSnapshot { protected calculateNetPerformancePercentages({}: {
accumulatedValuesByDate: { [date: string]: AccumulatedValues };
}): { [date: string]: NetPerformancePercentages } {
throw new Error('Method not implemented.');
}
protected calculateNetPerformancePercentagesForDateRange({}: {
historicalDataItems: HistoricalDataItem[];
}): { [date: string]: NetPerformancePercentages } {
throw new Error('Method not implemented.');
}
protected calculateOverallPerformance(): PortfolioSnapshot {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }

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

@ -1,8 +1,10 @@
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { PortfolioSnapshotComputationError } from '@ghostfolio/api/app/portfolio/errors/portfolio-snapshot-computation.error'; 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 { 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';
@ -67,7 +69,7 @@ import {
startOfYear, startOfYear,
subDays subDays
} from 'date-fns'; } from 'date-fns';
import { groupBy, isNumber, sortBy, sum, uniqBy } from 'lodash'; import { groupBy, isNumber, sortBy, uniqBy } from 'lodash';
export abstract class PortfolioCalculator { export abstract class PortfolioCalculator {
protected static readonly ENABLE_LOGGING = false; protected static readonly ENABLE_LOGGING = false;
@ -213,6 +215,18 @@ export abstract class PortfolioCalculator {
this.snapshotPromise.catch(() => undefined); 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( protected abstract calculateOverallPerformance(
positions: PortfolioCalculatorHolding[] positions: PortfolioCalculatorHolding[]
): PortfolioSnapshot; ): PortfolioSnapshot;
@ -230,6 +244,8 @@ export abstract class PortfolioCalculator {
activitiesCount: 0, activitiesCount: 0,
createdAt: new Date(), createdAt: new Date(),
currentValueInBaseCurrency: new Big(0), currentValueInBaseCurrency: new Big(0),
dividendYieldPercent: new Big(0),
dividendYieldPercentWithCurrencyEffect: new Big(0),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
historicalData: [], historicalData: [],
@ -353,19 +369,7 @@ export abstract class PortfolioCalculator {
const positions: PortfolioCalculatorHolding[] = []; const positions: PortfolioCalculatorHolding[] = [];
const accumulatedValuesByDate: { const accumulatedValuesByDate: {
[date: string]: { [date: string]: AccumulatedValues;
investmentValueWithCurrencyEffect: Big;
totalAverageInvestmentValue: Big;
totalAverageInvestmentValueWithCurrencyEffect: Big;
totalCashValueWithCurrencyEffect: Big;
totalCurrentValue: Big;
totalCurrentValueWithCurrencyEffect: Big;
totalInvestmentValue: Big;
totalInvestmentValueWithCurrencyEffect: Big;
totalNetPerformanceValue: Big;
totalNetPerformanceValueWithCurrencyEffect: Big;
totalNetWorthValueWithCurrencyEffect: Big;
};
} = {}; } = {};
const valuesByAssetProfileIdentifier: { const valuesByAssetProfileIdentifier: {
@ -409,6 +413,8 @@ export abstract class PortfolioCalculator {
averageInvestmentWithCurrencyEffect, averageInvestmentWithCurrencyEffect,
currentValues, currentValues,
currentValuesWithCurrencyEffect, currentValuesWithCurrencyEffect,
dividendYieldPercent,
dividendYieldPercentWithCurrencyEffect,
grossPerformance, grossPerformance,
grossPerformancePercentage, grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect, grossPerformancePercentageWithCurrencyEffect,
@ -477,6 +483,8 @@ export abstract class PortfolioCalculator {
positions.push({ positions.push({
averageInvestment, averageInvestment,
averageInvestmentWithCurrencyEffect, averageInvestmentWithCurrencyEffect,
dividendYieldPercent,
dividendYieldPercentWithCurrencyEffect,
activitiesCount: item.activitiesCount, activitiesCount: item.activitiesCount,
averagePrice: item.averagePrice, averagePrice: item.averagePrice,
currency: item.currency, currency: item.currency,
@ -647,13 +655,14 @@ export abstract class PortfolioCalculator {
} }
} }
const netPerformancePercentagesByDate =
this.calculateNetPerformancePercentages({ accumulatedValuesByDate });
const historicalData: HistoricalDataItem[] = Object.entries( const historicalData: HistoricalDataItem[] = Object.entries(
accumulatedValuesByDate accumulatedValuesByDate
).map(([date, values]) => { ).map(([date, values]) => {
const { const {
investmentValueWithCurrencyEffect, investmentValueWithCurrencyEffect,
totalAverageInvestmentValue,
totalAverageInvestmentValueWithCurrencyEffect,
totalCashValueWithCurrencyEffect, totalCashValueWithCurrencyEffect,
totalCurrentValue, totalCurrentValue,
totalCurrentValueWithCurrencyEffect, totalCurrentValueWithCurrencyEffect,
@ -664,21 +673,9 @@ export abstract class PortfolioCalculator {
totalNetWorthValueWithCurrencyEffect totalNetWorthValueWithCurrencyEffect
} = values; } = values;
const netPerformanceInPercentage = totalAverageInvestmentValue.eq(0)
? 0
: totalNetPerformanceValue.div(totalAverageInvestmentValue).toNumber();
const netPerformanceInPercentageWithCurrencyEffect =
totalAverageInvestmentValueWithCurrencyEffect.eq(0)
? 0
: totalNetPerformanceValueWithCurrencyEffect
.div(totalAverageInvestmentValueWithCurrencyEffect)
.toNumber();
return { return {
...netPerformancePercentagesByDate[date],
date, date,
netPerformanceInPercentage,
netPerformanceInPercentageWithCurrencyEffect,
investmentValueWithCurrencyEffect: investmentValueWithCurrencyEffect:
investmentValueWithCurrencyEffect.toNumber(), investmentValueWithCurrencyEffect.toNumber(),
netPerformance: totalNetPerformanceValue.toNumber(), netPerformance: totalNetPerformanceValue.toNumber(),
@ -872,6 +869,8 @@ export abstract class PortfolioCalculator {
averageInvestmentWithCurrencyEffect: new Big(0), averageInvestmentWithCurrencyEffect: new Big(0),
currentValues: {}, currentValues: {},
currentValuesWithCurrencyEffect: {}, currentValuesWithCurrencyEffect: {},
dividendYieldPercent: new Big(0),
dividendYieldPercentWithCurrencyEffect: new Big(0),
grossPerformance: new Big(0), grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0), grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0),
@ -1342,12 +1341,8 @@ export abstract class PortfolioCalculator {
const { historicalData } = this.snapshot; 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 netPerformanceAtStartDate: number;
let netPerformanceWithCurrencyEffectAtStartDate: number; let netPerformanceWithCurrencyEffectAtStartDate: number;
@ -1356,71 +1351,35 @@ export abstract class PortfolioCalculator {
if (!isBefore(date, start) && !isAfter(date, end)) { if (!isBefore(date, start) && !isAfter(date, end)) {
if (!isNumber(netPerformanceAtStartDate)) { if (!isNumber(netPerformanceAtStartDate)) {
grossPerformanceAtStartDate =
historicalDataItem.value - historicalDataItem.totalInvestment;
grossPerformanceWithCurrencyEffectAtStartDate =
historicalDataItem.valueWithCurrencyEffect -
historicalDataItem.totalInvestmentValueWithCurrencyEffect;
netPerformanceAtStartDate = historicalDataItem.netPerformance; netPerformanceAtStartDate = historicalDataItem.netPerformance;
netPerformanceWithCurrencyEffectAtStartDate = netPerformanceWithCurrencyEffectAtStartDate =
historicalDataItem.netPerformanceWithCurrencyEffect; historicalDataItem.netPerformanceWithCurrencyEffect;
} }
const netPerformanceSinceStartDate = historicalDataItemsOfDateRange.push({
historicalDataItem.netPerformance - netPerformanceAtStartDate;
const netPerformanceWithCurrencyEffectSinceStartDate =
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
);
}
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;
chart.push({
...historicalDataItem, ...historicalDataItem,
netPerformance: netPerformanceSinceStartDate, netPerformance:
historicalDataItem.netPerformance - netPerformanceAtStartDate,
netPerformanceWithCurrencyEffect: netPerformanceWithCurrencyEffect:
netPerformanceWithCurrencyEffectSinceStartDate, historicalDataItem.netPerformanceWithCurrencyEffect -
netPerformanceInPercentage: netPerformanceWithCurrencyEffectAtStartDate
averageInvestmentValue > 0
? netPerformanceSinceStartDate / averageInvestmentValue
: 0,
netPerformanceInPercentageWithCurrencyEffect:
averageInvestmentValueWithCurrencyEffect > 0
? netPerformanceWithCurrencyEffectSinceStartDate /
averageInvestmentValueWithCurrencyEffect
: 0
}); });
} }
} }
const netPerformancePercentagesByDate =
this.calculateNetPerformancePercentagesForDateRange({
historicalDataItems: historicalDataItemsOfDateRange
});
const chart = historicalDataItemsOfDateRange.map((historicalDataItem) => {
return {
...historicalDataItem,
...netPerformancePercentagesByDate[historicalDataItem.date]
};
});
return { chart }; 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', dateOfFirstActivity: '2023-12-31',
dividend: new Big(0), dividend: new Big(0),
dividendInBaseCurrency: new Big(0), dividendInBaseCurrency: new Big(0),
dividendYieldPercent: new Big(0),
dividendYieldPercentWithCurrencyEffect: new Big(0),
fee: new Big(0), fee: new Big(0),
feeInBaseCurrency: new Big(0), feeInBaseCurrency: new Big(0),
grossPerformance: 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({ expect(portfolioSnapshot).toMatchObject({
dividendYieldPercent: new Big('0.001144362748184'),
dividendYieldPercentWithCurrencyEffect: new Big('0.001144362748184'),
errors: [], errors: [],
hasErrors: false, hasErrors: false,
positions: [ positions: [
@ -148,6 +150,10 @@ describe('PortfolioCalculator', () => {
dateOfFirstActivity: '2021-09-16', dateOfFirstActivity: '2021-09-16',
dividend: new Big('0.62'), dividend: new Big('0.62'),
dividendInBaseCurrency: new Big('0.62'), dividendInBaseCurrency: new Big('0.62'),
dividendYieldPercent: new Big('0.001144362748184'),
dividendYieldPercentWithCurrencyEffect: new Big(
'0.001144362748184'
),
fee: new Big('19'), fee: new Big('19'),
grossPerformance: new Big('33.25'), grossPerformance: new Big('33.25'),
grossPerformancePercentage: new Big('0.11136043941322258691'), grossPerformancePercentage: new Big('0.11136043941322258691'),

191
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 { 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 { 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 { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import {
getAnnualizedPerformancePercent,
getIntervalFromDateRange
} from '@ghostfolio/common/calculation-helper';
import { import {
DATE_FORMAT, DATE_FORMAT,
getAssetProfileIdentifier, getAssetProfileIdentifier,
parseDate parseDate
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import {
AssetProfileIdentifier,
HistoricalDataItem
} from '@ghostfolio/common/interfaces';
import { PortfolioSnapshot } from '@ghostfolio/common/models'; import { PortfolioSnapshot } from '@ghostfolio/common/models';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
@ -21,18 +29,120 @@ import {
isBefore, isBefore,
isThisYear isThisYear
} from 'date-fns'; } from 'date-fns';
import { isNumber, sum } from 'lodash';
export class RoaiPortfolioCalculator extends PortfolioCalculator { 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 historicalDataItem of historicalDataItems) {
if (!isNumber(grossPerformanceAtStartDate)) {
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( protected calculateOverallPerformance(
positions: PortfolioCalculatorHolding[] positions: PortfolioCalculatorHolding[]
): PortfolioSnapshot { ): PortfolioSnapshot {
let currentValueInBaseCurrency = new Big(0); let currentValueInBaseCurrency = new Big(0);
let grossPerformance = new Big(0);
let grossPerformanceWithCurrencyEffect = new Big(0);
let hasErrors = false; let hasErrors = false;
let netPerformance = new Big(0);
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);
@ -51,6 +161,12 @@ 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
@ -68,18 +184,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
hasErrors = true; hasErrors = true;
} }
if (currentPosition.grossPerformance) { if (
grossPerformance = grossPerformance.plus( !currentPosition.grossPerformance &&
currentPosition.grossPerformance !currentPosition.quantity.eq(0)
); ) {
grossPerformanceWithCurrencyEffect =
grossPerformanceWithCurrencyEffect.plus(
currentPosition.grossPerformanceWithCurrencyEffect
);
netPerformance = netPerformance.plus(currentPosition.netPerformance);
} else if (!currentPosition.quantity.eq(0)) {
hasErrors = true; hasErrors = true;
} }
@ -101,8 +209,33 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
} }
} }
const dateOfFirstActivity = this.getStartDate();
const daysInMarket = dateOfFirstActivity
? differenceInDays(new Date(), dateOfFirstActivity)
: 0;
const dividendYieldPercent = getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercentage: totalAverageInvestment.eq(0)
? new Big(0)
: totalDividendInBaseCurrency.div(totalAverageInvestment)
});
const dividendYieldPercentWithCurrencyEffect =
getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercentage: totalAverageInvestmentWithCurrencyEffect.eq(0)
? new Big(0)
: totalDividendInBaseCurrency.div(
totalAverageInvestmentWithCurrencyEffect
)
});
return { return {
currentValueInBaseCurrency, currentValueInBaseCurrency,
dividendYieldPercent,
dividendYieldPercentWithCurrencyEffect,
hasErrors, hasErrors,
positions, positions,
totalFeesWithCurrencyEffect, totalFeesWithCurrencyEffect,
@ -401,6 +534,28 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
? totalNetPerformance.div(averageInvestmentBetweenStartAndEndDate) ? totalNetPerformance.div(averageInvestmentBetweenStartAndEndDate)
: new Big(0); : 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: { const netPerformancePercentageWithCurrencyEffectMap: {
[key: DateRange]: Big; [key: DateRange]: Big;
} = {}; } = {};
@ -538,6 +693,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
averageInvestmentValuesWithCurrencyEffect, averageInvestmentValuesWithCurrencyEffect,
currentValues, currentValues,
currentValuesWithCurrencyEffect, currentValuesWithCurrencyEffect,
dividendYieldPercent,
dividendYieldPercentWithCurrencyEffect,
grossPerformancePercentage, grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect, grossPerformancePercentageWithCurrencyEffect,
investmentValuesAccumulated, investmentValuesAccumulated,

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

@ -1,12 +1,28 @@
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 { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface'; import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import {
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 calculateOverallPerformance([]: PortfolioCalculatorHolding[]): PortfolioSnapshot { protected calculateNetPerformancePercentages({}: {
accumulatedValuesByDate: { [date: string]: AccumulatedValues };
}): { [date: string]: NetPerformancePercentages } {
throw new Error('Method not implemented.');
}
protected calculateNetPerformancePercentagesForDateRange({}: {
historicalDataItems: HistoricalDataItem[];
}): { [date: string]: NetPerformancePercentages } {
throw new Error('Method not implemented.');
}
protected calculateOverallPerformance(): PortfolioSnapshot {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }

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

@ -1,12 +1,28 @@
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 { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface'; import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import {
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 calculateOverallPerformance([]: PortfolioCalculatorHolding[]): PortfolioSnapshot { protected calculateNetPerformancePercentages({}: {
accumulatedValuesByDate: { [date: string]: AccumulatedValues };
}): { [date: string]: NetPerformancePercentages } {
throw new Error('Method not implemented.');
}
protected calculateNetPerformancePercentagesForDateRange({}: {
historicalDataItems: HistoricalDataItem[];
}): { [date: string]: NetPerformancePercentages } {
throw new Error('Method not implemented.');
}
protected calculateOverallPerformance(): PortfolioSnapshot {
throw new Error('Method not implemented.'); 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: { currentValuesWithCurrencyEffect: {
[date: string]: Big; [date: string]: Big;
}; };
dividendYieldPercent: Big;
dividendYieldPercentWithCurrencyEffect: Big;
grossPerformance: Big; grossPerformance: Big;
grossPerformancePercentage: Big; grossPerformancePercentage: Big;
grossPerformancePercentageWithCurrencyEffect: Big; grossPerformancePercentageWithCurrencyEffect: Big;

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

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

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

@ -984,12 +984,12 @@ export class PortfolioService {
const { const {
activitiesCount, activitiesCount,
averageInvestment,
averageInvestmentWithCurrencyEffect,
averagePrice, averagePrice,
currency, currency,
dateOfFirstActivity, dateOfFirstActivity,
dividendInBaseCurrency, dividendInBaseCurrency,
dividendYieldPercent,
dividendYieldPercentWithCurrencyEffect,
feeInBaseCurrency, feeInBaseCurrency,
grossPerformance, grossPerformance,
grossPerformancePercentage, grossPerformancePercentage,
@ -1012,27 +1012,6 @@ export class PortfolioService {
); );
}); });
const dividendYieldPercent = getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(
new Date(),
parseDate(dateOfFirstActivity)
),
netPerformancePercentage: averageInvestment.eq(0)
? new Big(0)
: dividendInBaseCurrency.div(averageInvestment)
});
const dividendYieldPercentWithCurrencyEffect =
getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(
new Date(),
parseDate(dateOfFirstActivity)
),
netPerformancePercentage: averageInvestmentWithCurrencyEffect.eq(0)
? new Big(0)
: dividendInBaseCurrency.div(averageInvestmentWithCurrencyEffect)
});
const historicalData = await this.dataProviderService.getHistorical( const historicalData = await this.dataProviderService.getHistorical(
[{ dataSource, symbol }], [{ dataSource, symbol }],
'day', 'day',
@ -1138,9 +1117,11 @@ export class PortfolioService {
.getDataProvider(dataSource) .getDataProvider(dataSource)
.getDataProviderInfo(), .getDataProviderInfo(),
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
dividendYieldPercent: dividendYieldPercent.toNumber(), // TODO: Remove the fallback with the next release, when the cached
// portfolio snapshots have been recalculated
dividendYieldPercent: dividendYieldPercent?.toNumber() ?? 0,
dividendYieldPercentWithCurrencyEffect: dividendYieldPercentWithCurrencyEffect:
dividendYieldPercentWithCurrencyEffect.toNumber(), dividendYieldPercentWithCurrencyEffect?.toNumber() ?? 0,
feeInBaseCurrency: feeInBaseCurrency.toNumber(), feeInBaseCurrency: feeInBaseCurrency.toNumber(),
grossPerformance: grossPerformance?.toNumber(), grossPerformance: grossPerformance?.toNumber(),
grossPerformancePercent: grossPerformancePercentage?.toNumber(), grossPerformancePercent: grossPerformancePercentage?.toNumber(),
@ -2097,6 +2078,8 @@ export class PortfolioService {
} }
const { const {
dividendYieldPercent,
dividendYieldPercentWithCurrencyEffect,
totalCashInBaseCurrency, totalCashInBaseCurrency,
totalInvestment, totalInvestment,
totalInvestmentWithCurrencyEffect, totalInvestmentWithCurrencyEffect,
@ -2219,6 +2202,11 @@ 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
// portfolio snapshots have been recalculated
dividendYieldPercent: dividendYieldPercent?.toNumber() ?? 0,
dividendYieldPercentWithCurrencyEffect:
dividendYieldPercentWithCurrencyEffect?.toNumber() ?? 0,
emergencyFund: { emergencyFund: {
assets: emergencyFundHoldingsValueInBaseCurrency, assets: emergencyFundHoldingsValueInBaseCurrency,
cash: totalEmergencyFund cash: totalEmergencyFund

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

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

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

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

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

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

Loading…
Cancel
Save