|
|
@ -2,19 +2,10 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/po |
|
|
import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface'; |
|
|
import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface'; |
|
|
import { AverageInvestmentItem } from '@ghostfolio/api/app/portfolio/interfaces/average-investment-item.interface'; |
|
|
import { AverageInvestmentItem } from '@ghostfolio/api/app/portfolio/interfaces/average-investment-item.interface'; |
|
|
import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.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 { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface'; |
|
|
import { WeightedInvestment } from '@ghostfolio/api/app/portfolio/interfaces/weighted-investment.interface'; |
|
|
import { WeightedInvestment } from '@ghostfolio/api/app/portfolio/interfaces/weighted-investment.interface'; |
|
|
import { PerformancePercentages } from '@ghostfolio/api/app/portfolio/types/performance-percentages.type'; |
|
|
import { PerformancePercentages } from '@ghostfolio/api/app/portfolio/types/performance-percentages.type'; |
|
|
import { |
|
|
import { getSum, parseDate } from '@ghostfolio/common/helper'; |
|
|
getAnnualizedPerformancePercent, |
|
|
|
|
|
getIntervalFromDateRange |
|
|
|
|
|
} from '@ghostfolio/common/calculation-helper'; |
|
|
|
|
|
import { |
|
|
|
|
|
DATE_FORMAT, |
|
|
|
|
|
getAssetProfileIdentifier, |
|
|
|
|
|
parseDate |
|
|
|
|
|
} from '@ghostfolio/common/helper'; |
|
|
|
|
|
import { |
|
|
import { |
|
|
AssetProfileIdentifier, |
|
|
AssetProfileIdentifier, |
|
|
HistoricalDataItem |
|
|
HistoricalDataItem |
|
|
@ -24,144 +15,39 @@ 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'; |
|
|
|
|
|
|
|
|
import { Big } from 'big.js'; |
|
|
import { Big } from 'big.js'; |
|
|
import { |
|
|
import { differenceInDays } from 'date-fns'; |
|
|
differenceInDays, |
|
|
import { mapValues, sortedIndex } from 'lodash-es'; |
|
|
eachYearOfInterval, |
|
|
|
|
|
format, |
|
|
|
|
|
isBefore, |
|
|
|
|
|
isThisYear |
|
|
|
|
|
} from 'date-fns'; |
|
|
|
|
|
import { sortedIndex } from 'lodash-es'; |
|
|
|
|
|
|
|
|
|
|
|
export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|
|
export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|
|
protected calculateOverallPerformance( |
|
|
protected calculateOverallPerformance({ |
|
|
positions: PortfolioCalculatorHolding[] |
|
|
positions |
|
|
): PortfolioSnapshot { |
|
|
}: { |
|
|
let currentValueInBaseCurrency = new Big(0); |
|
|
accumulatedValuesByDate: { [date: string]: AccumulatedValues }; |
|
|
let hasErrors = false; |
|
|
positions: PortfolioCalculatorHolding[]; |
|
|
let totalAverageInvestment = new Big(0); |
|
|
}): PortfolioSnapshot { |
|
|
let totalAverageInvestmentWithCurrencyEffect = new Big(0); |
|
|
// The invested capital of the portfolio is the sum of the average
|
|
|
let totalFeesWithCurrencyEffect = new Big(0); |
|
|
// investments of the holdings
|
|
|
const totalInterestWithCurrencyEffect = new Big(0); |
|
|
const positionsIncludedInPerformance = positions.filter( |
|
|
let totalInvestment = new Big(0); |
|
|
({ includeInPerformance }) => { |
|
|
let totalInvestmentWithCurrencyEffect = new Big(0); |
|
|
return includeInPerformance; |
|
|
|
|
|
|
|
|
for (const currentPosition of positions) { |
|
|
|
|
|
if (currentPosition.valueInBaseCurrency) { |
|
|
|
|
|
currentValueInBaseCurrency = currentValueInBaseCurrency.plus( |
|
|
|
|
|
currentPosition.valueInBaseCurrency |
|
|
|
|
|
); |
|
|
|
|
|
} else { |
|
|
|
|
|
hasErrors = true; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!currentPosition.includeInPerformance) { |
|
|
|
|
|
continue; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (currentPosition.feeInBaseCurrency) { |
|
|
|
|
|
totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( |
|
|
|
|
|
currentPosition.feeInBaseCurrency |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (currentPosition.investment) { |
|
|
|
|
|
totalInvestment = totalInvestment.plus(currentPosition.investment); |
|
|
|
|
|
|
|
|
|
|
|
totalInvestmentWithCurrencyEffect = |
|
|
|
|
|
totalInvestmentWithCurrencyEffect.plus( |
|
|
|
|
|
currentPosition.investmentWithCurrencyEffect |
|
|
|
|
|
); |
|
|
|
|
|
} else { |
|
|
|
|
|
hasErrors = true; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
!currentPosition.grossPerformance && |
|
|
|
|
|
!currentPosition.quantity.eq(0) |
|
|
|
|
|
) { |
|
|
|
|
|
hasErrors = true; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (currentPosition.averageInvestment) { |
|
|
|
|
|
totalAverageInvestment = totalAverageInvestment.plus( |
|
|
|
|
|
currentPosition.averageInvestment |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
totalAverageInvestmentWithCurrencyEffect = |
|
|
|
|
|
totalAverageInvestmentWithCurrencyEffect.plus( |
|
|
|
|
|
currentPosition.averageInvestmentWithCurrencyEffect |
|
|
|
|
|
); |
|
|
|
|
|
} else if (!currentPosition.quantity.eq(0)) { |
|
|
|
|
|
this.logger.warn( |
|
|
|
|
|
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})` |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
hasErrors = true; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const dateOfFirstActivity = this.getStartDate(); |
|
|
|
|
|
|
|
|
|
|
|
const daysInMarket = dateOfFirstActivity |
|
|
|
|
|
? differenceInDays(new Date(), dateOfFirstActivity) |
|
|
|
|
|
: 0; |
|
|
|
|
|
|
|
|
|
|
|
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({ |
|
|
return this.getOverallPerformance({ |
|
|
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, |
|
|
positions, |
|
|
totalFeesWithCurrencyEffect, |
|
|
investedCapital: getSum( |
|
|
totalInterestWithCurrencyEffect, |
|
|
positionsIncludedInPerformance.map(({ averageInvestment }) => { |
|
|
totalInvestment, |
|
|
return averageInvestment; |
|
|
totalInvestmentWithCurrencyEffect, |
|
|
}) |
|
|
activitiesCount: this.activities.filter(({ type }) => { |
|
|
), |
|
|
return ['BUY', 'SELL'].includes(type); |
|
|
investedCapitalWithCurrencyEffect: getSum( |
|
|
}).length, |
|
|
positionsIncludedInPerformance.map( |
|
|
createdAt: new Date(), |
|
|
({ averageInvestmentWithCurrencyEffect }) => { |
|
|
errors: [], |
|
|
return averageInvestmentWithCurrencyEffect; |
|
|
historicalData: [], |
|
|
} |
|
|
totalCashInBaseCurrency: new Big(0), |
|
|
) |
|
|
totalLiabilitiesWithCurrencyEffect: new Big(0) |
|
|
) |
|
|
}; |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
protected calculatePerformancePercentages({ |
|
|
protected calculatePerformancePercentages({ |
|
|
@ -169,42 +55,14 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|
|
}: { |
|
|
}: { |
|
|
accumulatedValuesByDate: { [date: string]: AccumulatedValues }; |
|
|
accumulatedValuesByDate: { [date: string]: AccumulatedValues }; |
|
|
}): { [date: string]: PerformancePercentages } { |
|
|
}): { [date: string]: PerformancePercentages } { |
|
|
const performancePercentagesByDate: { |
|
|
return mapValues(accumulatedValuesByDate, (accumulatedValues) => { |
|
|
[date: string]: PerformancePercentages; |
|
|
return this.getPerformancePercentagesOfAccumulatedValues({ |
|
|
} = {}; |
|
|
accumulatedValues, |
|
|
|
|
|
investedCapital: accumulatedValues.totalAverageInvestmentValue, |
|
|
for (const [ |
|
|
investedCapitalWithCurrencyEffect: |
|
|
date, |
|
|
accumulatedValues.totalAverageInvestmentValueWithCurrencyEffect |
|
|
{ |
|
|
}); |
|
|
totalAverageInvestmentValue, |
|
|
}); |
|
|
totalAverageInvestmentValueWithCurrencyEffect, |
|
|
|
|
|
totalDividendValueWithCurrencyEffect, |
|
|
|
|
|
totalNetPerformanceValue, |
|
|
|
|
|
totalNetPerformanceValueWithCurrencyEffect |
|
|
|
|
|
} |
|
|
|
|
|
] of Object.entries(accumulatedValuesByDate)) { |
|
|
|
|
|
performancePercentagesByDate[date] = { |
|
|
|
|
|
dividendInPercentageWithCurrencyEffect: |
|
|
|
|
|
totalAverageInvestmentValueWithCurrencyEffect.eq(0) |
|
|
|
|
|
? 0 |
|
|
|
|
|
: totalDividendValueWithCurrencyEffect |
|
|
|
|
|
.div(totalAverageInvestmentValueWithCurrencyEffect) |
|
|
|
|
|
.toNumber(), |
|
|
|
|
|
netPerformanceInPercentage: totalAverageInvestmentValue.eq(0) |
|
|
|
|
|
? 0 |
|
|
|
|
|
: totalNetPerformanceValue |
|
|
|
|
|
.div(totalAverageInvestmentValue) |
|
|
|
|
|
.toNumber(), |
|
|
|
|
|
netPerformanceInPercentageWithCurrencyEffect: |
|
|
|
|
|
totalAverageInvestmentValueWithCurrencyEffect.eq(0) |
|
|
|
|
|
? 0 |
|
|
|
|
|
: totalNetPerformanceValueWithCurrencyEffect |
|
|
|
|
|
.div(totalAverageInvestmentValueWithCurrencyEffect) |
|
|
|
|
|
.toNumber() |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return performancePercentagesByDate; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
protected calculatePerformancePercentagesForDateRange({ |
|
|
protected calculatePerformancePercentagesForDateRange({ |
|
|
@ -258,31 +116,16 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
for (const [index, historicalDataItem] of historicalDataItems.entries()) { |
|
|
for (const [index, historicalDataItem] of historicalDataItems.entries()) { |
|
|
const averageInvestmentValue = this.getAverageInvestment( |
|
|
performancePercentagesByDate[historicalDataItem.date] = |
|
|
weightedInvestments[index] |
|
|
this.getPerformancePercentagesOfHistoricalDataItem({ |
|
|
).toNumber(); |
|
|
historicalDataItem, |
|
|
|
|
|
investedCapital: this.getAverageInvestment( |
|
|
const averageInvestmentValueWithCurrencyEffect = |
|
|
weightedInvestments[index] |
|
|
this.getAverageInvestment( |
|
|
).toNumber(), |
|
|
weightedInvestmentsWithCurrencyEffect[index] |
|
|
investedCapitalWithCurrencyEffect: this.getAverageInvestment( |
|
|
).toNumber(); |
|
|
weightedInvestmentsWithCurrencyEffect[index] |
|
|
|
|
|
).toNumber() |
|
|
performancePercentagesByDate[historicalDataItem.date] = { |
|
|
}); |
|
|
dividendInPercentageWithCurrencyEffect: |
|
|
|
|
|
averageInvestmentValueWithCurrencyEffect > 0 |
|
|
|
|
|
? historicalDataItem.dividendInBaseCurrency / |
|
|
|
|
|
averageInvestmentValueWithCurrencyEffect |
|
|
|
|
|
: 0, |
|
|
|
|
|
netPerformanceInPercentage: |
|
|
|
|
|
averageInvestmentValue > 0 |
|
|
|
|
|
? historicalDataItem.netPerformance / averageInvestmentValue |
|
|
|
|
|
: 0, |
|
|
|
|
|
netPerformanceInPercentageWithCurrencyEffect: |
|
|
|
|
|
averageInvestmentValueWithCurrencyEffect > 0 |
|
|
|
|
|
? historicalDataItem.netPerformanceWithCurrencyEffect / |
|
|
|
|
|
averageInvestmentValueWithCurrencyEffect |
|
|
|
|
|
: 0 |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return performancePercentagesByDate; |
|
|
return performancePercentagesByDate; |
|
|
@ -307,6 +150,31 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|
|
}; |
|
|
}; |
|
|
start: Date; |
|
|
start: Date; |
|
|
} & AssetProfileIdentifier): HoldingPerformance { |
|
|
} & AssetProfileIdentifier): HoldingPerformance { |
|
|
|
|
|
const { holdingPerformance, valuation } = |
|
|
|
|
|
this.getMethodIndependentHoldingPerformance({ |
|
|
|
|
|
chartDates, |
|
|
|
|
|
dataSource, |
|
|
|
|
|
end, |
|
|
|
|
|
exchangeRates, |
|
|
|
|
|
marketSymbolMap, |
|
|
|
|
|
start, |
|
|
|
|
|
symbol |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if (!valuation) { |
|
|
|
|
|
return holdingPerformance; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const { |
|
|
|
|
|
currentValuesWithCurrencyEffect, |
|
|
|
|
|
dateRanges, |
|
|
|
|
|
daysInMarket, |
|
|
|
|
|
indexOfEndActivity, |
|
|
|
|
|
indexOfStartActivity, |
|
|
|
|
|
investmentValuesAccumulatedWithCurrencyEffect, |
|
|
|
|
|
items |
|
|
|
|
|
} = valuation; |
|
|
|
|
|
|
|
|
const averageInvestmentValues: { [date: string]: Big } = {}; |
|
|
const averageInvestmentValues: { [date: string]: Big } = {}; |
|
|
|
|
|
|
|
|
const averageInvestmentValuesWithCurrencyEffect: { |
|
|
const averageInvestmentValuesWithCurrencyEffect: { |
|
|
@ -318,107 +186,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|
|
let valueAtStartDate: Big; |
|
|
let valueAtStartDate: Big; |
|
|
let valueAtStartDateWithCurrencyEffect: Big; |
|
|
let valueAtStartDateWithCurrencyEffect: Big; |
|
|
|
|
|
|
|
|
const assetProfileIdentifier = getAssetProfileIdentifier({ |
|
|
|
|
|
dataSource, |
|
|
|
|
|
symbol |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
let activities: PortfolioCalculatorActivityItem[] = |
|
|
|
|
|
this.activitiesByAssetProfileIdentifier[assetProfileIdentifier] ?? []; |
|
|
|
|
|
|
|
|
|
|
|
const isCash = activities[0]?.assetProfile?.assetSubClass === 'CASH'; |
|
|
|
|
|
|
|
|
|
|
|
if (activities.length <= 0) { |
|
|
|
|
|
return this.getEmptyHoldingPerformance(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// The dividends, the interest and the liabilities are derived from the
|
|
|
|
|
|
// activities only. Accumulate them upfront so that they survive the bail
|
|
|
|
|
|
// out for symbols without a market price below.
|
|
|
|
|
|
const { |
|
|
|
|
|
totalDividend, |
|
|
|
|
|
totalDividendInBaseCurrency, |
|
|
|
|
|
totalInterestInBaseCurrency, |
|
|
|
|
|
totalLiabilitiesInBaseCurrency |
|
|
|
|
|
} = this.getTotalsFromActivities({ activities, exchangeRates }); |
|
|
|
|
|
|
|
|
|
|
|
const dateOfFirstActivity = parseDate(activities[0].date); |
|
|
|
|
|
|
|
|
|
|
|
const endDateString = format(end, DATE_FORMAT); |
|
|
|
|
|
const startDateString = format(start, DATE_FORMAT); |
|
|
|
|
|
|
|
|
|
|
|
const unitPriceAtStartDate = |
|
|
|
|
|
marketSymbolMap[startDateString]?.[assetProfileIdentifier]; |
|
|
|
|
|
|
|
|
|
|
|
const unitPriceAtEndDate = this.getUnitPriceAtEndDate({ |
|
|
|
|
|
activities, |
|
|
|
|
|
dataSource, |
|
|
|
|
|
isCash, |
|
|
|
|
|
marketPriceAtEndDate: |
|
|
|
|
|
marketSymbolMap[endDateString]?.[assetProfileIdentifier] |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
!unitPriceAtEndDate || |
|
|
|
|
|
(!unitPriceAtStartDate && isBefore(dateOfFirstActivity, start)) |
|
|
|
|
|
) { |
|
|
|
|
|
// A missing market price can only affect the quantity which is held. The
|
|
|
|
|
|
// dividends, the interest and the liabilities do not hold any quantity
|
|
|
|
|
|
// and are therefore not in error.
|
|
|
|
|
|
const hasActivitiesWithQuantity = activities.some(({ type }) => { |
|
|
|
|
|
return ['BUY', 'SELL'].includes(type); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
...this.getEmptyHoldingPerformance(), |
|
|
|
|
|
totalDividend, |
|
|
|
|
|
totalDividendInBaseCurrency, |
|
|
|
|
|
totalInterestInBaseCurrency, |
|
|
|
|
|
totalLiabilitiesInBaseCurrency, |
|
|
|
|
|
hasErrors: hasActivitiesWithQuantity |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
activities = this.getActivitiesWithMarketPrices({ |
|
|
|
|
|
activities, |
|
|
|
|
|
chartDates, |
|
|
|
|
|
endDateString, |
|
|
|
|
|
marketSymbolMap, |
|
|
|
|
|
startDateString, |
|
|
|
|
|
unitPriceAtEndDate, |
|
|
|
|
|
unitPriceAtStartDate, |
|
|
|
|
|
assetProfile: { |
|
|
|
|
|
dataSource, |
|
|
|
|
|
symbol, |
|
|
|
|
|
assetSubClass: isCash ? 'CASH' : undefined |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const { |
|
|
|
|
|
currentValues, |
|
|
|
|
|
currentValuesWithCurrencyEffect, |
|
|
|
|
|
dividendValuesWithCurrencyEffect, |
|
|
|
|
|
initialValue, |
|
|
|
|
|
investmentValuesAccumulated, |
|
|
|
|
|
investmentValuesAccumulatedWithCurrencyEffect, |
|
|
|
|
|
investmentValuesWithCurrencyEffect, |
|
|
|
|
|
items, |
|
|
|
|
|
netPerformanceValues, |
|
|
|
|
|
netPerformanceValuesWithCurrencyEffect |
|
|
|
|
|
} = this.getHoldingValuation({ |
|
|
|
|
|
activities, |
|
|
|
|
|
exchangeRates, |
|
|
|
|
|
unitPriceAtStartDate |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const indexOfStartActivity = items.findIndex(({ itemType }) => { |
|
|
|
|
|
return itemType === 'start'; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const indexOfEndActivity = items.findIndex(({ itemType }) => { |
|
|
|
|
|
return itemType === 'end'; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
let sumOfWeightedInvestments = new Big(0); |
|
|
let sumOfWeightedInvestments = new Big(0); |
|
|
let sumOfWeightedInvestmentsWithCurrencyEffect = new Big(0); |
|
|
let sumOfWeightedInvestmentsWithCurrencyEffect = new Big(0); |
|
|
let totalInvestmentDays = 0; |
|
|
let totalInvestmentDays = 0; |
|
|
@ -503,37 +270,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const { |
|
|
|
|
|
fees: feesAtStartDate, |
|
|
|
|
|
feesWithCurrencyEffect: feesAtStartDateWithCurrencyEffect, |
|
|
|
|
|
grossPerformance: grossPerformanceAtStartDate, |
|
|
|
|
|
grossPerformanceWithCurrencyEffect: |
|
|
|
|
|
grossPerformanceAtStartDateWithCurrencyEffect |
|
|
|
|
|
} = items[indexOfStartActivity]; |
|
|
|
|
|
|
|
|
|
|
|
const { |
|
|
|
|
|
fees, |
|
|
|
|
|
feesWithCurrencyEffect, |
|
|
|
|
|
grossPerformance, |
|
|
|
|
|
grossPerformanceWithCurrencyEffect, |
|
|
|
|
|
investment: totalInvestment, |
|
|
|
|
|
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect, |
|
|
|
|
|
quantity: totalQuantity |
|
|
|
|
|
} = items[indexOfEndActivity]; |
|
|
|
|
|
|
|
|
|
|
|
const totalGrossPerformance = grossPerformance.minus( |
|
|
|
|
|
grossPerformanceAtStartDate |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const totalGrossPerformanceWithCurrencyEffect = |
|
|
|
|
|
grossPerformanceWithCurrencyEffect.minus( |
|
|
|
|
|
grossPerformanceAtStartDateWithCurrencyEffect |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const totalNetPerformance = grossPerformance |
|
|
|
|
|
.minus(grossPerformanceAtStartDate) |
|
|
|
|
|
.minus(fees.minus(feesAtStartDate)); |
|
|
|
|
|
|
|
|
|
|
|
const averageInvestmentBetweenStartAndEndDate = |
|
|
const averageInvestmentBetweenStartAndEndDate = |
|
|
totalInvestmentDays > 0 |
|
|
totalInvestmentDays > 0 |
|
|
? sumOfWeightedInvestments.div(totalInvestmentDays) |
|
|
? sumOfWeightedInvestments.div(totalInvestmentDays) |
|
|
@ -544,91 +280,13 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|
|
? sumOfWeightedInvestmentsWithCurrencyEffect.div(totalInvestmentDays) |
|
|
? sumOfWeightedInvestmentsWithCurrencyEffect.div(totalInvestmentDays) |
|
|
: new Big(0); |
|
|
: new Big(0); |
|
|
|
|
|
|
|
|
const grossPerformancePercentage = |
|
|
const averageInvestmentByDateRange: { [key: DateRange]: Big } = {}; |
|
|
averageInvestmentBetweenStartAndEndDate.gt(0) |
|
|
|
|
|
? totalGrossPerformance.div(averageInvestmentBetweenStartAndEndDate) |
|
|
|
|
|
: new Big(0); |
|
|
|
|
|
|
|
|
|
|
|
const grossPerformancePercentageWithCurrencyEffect = |
|
|
|
|
|
averageInvestmentBetweenStartAndEndDateWithCurrencyEffect.gt(0) |
|
|
|
|
|
? totalGrossPerformanceWithCurrencyEffect.div( |
|
|
|
|
|
averageInvestmentBetweenStartAndEndDateWithCurrencyEffect |
|
|
|
|
|
) |
|
|
|
|
|
: new Big(0); |
|
|
|
|
|
|
|
|
|
|
|
const feesPerUnit = totalQuantity.gt(0) |
|
|
|
|
|
? fees.minus(feesAtStartDate).div(totalQuantity) |
|
|
|
|
|
: new Big(0); |
|
|
|
|
|
|
|
|
|
|
|
const feesPerUnitWithCurrencyEffect = totalQuantity.gt(0) |
|
|
|
|
|
? feesWithCurrencyEffect |
|
|
|
|
|
.minus(feesAtStartDateWithCurrencyEffect) |
|
|
|
|
|
.div(totalQuantity) |
|
|
|
|
|
: new Big(0); |
|
|
|
|
|
|
|
|
|
|
|
const netPerformancePercentage = averageInvestmentBetweenStartAndEndDate.gt( |
|
|
|
|
|
0 |
|
|
|
|
|
) |
|
|
|
|
|
? 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; |
|
|
|
|
|
} = {}; |
|
|
|
|
|
|
|
|
|
|
|
const netPerformanceWithCurrencyEffectMap: { |
|
|
|
|
|
[key: DateRange]: Big; |
|
|
|
|
|
} = {}; |
|
|
|
|
|
|
|
|
|
|
|
for (const dateRange of [ |
|
|
|
|
|
'1d', |
|
|
|
|
|
'1y', |
|
|
|
|
|
'5y', |
|
|
|
|
|
'max', |
|
|
|
|
|
'mtd', |
|
|
|
|
|
'wtd', |
|
|
|
|
|
'ytd', |
|
|
|
|
|
...eachYearOfInterval({ end, start }) |
|
|
|
|
|
.filter((date) => { |
|
|
|
|
|
return !isThisYear(date); |
|
|
|
|
|
}) |
|
|
|
|
|
.map((date) => { |
|
|
|
|
|
return format(date, 'yyyy'); |
|
|
|
|
|
}) |
|
|
|
|
|
] as DateRange[]) { |
|
|
|
|
|
const dateInterval = getIntervalFromDateRange({ dateRange }); |
|
|
|
|
|
const endDate = dateInterval.endDate; |
|
|
|
|
|
let startDate = dateInterval.startDate; |
|
|
|
|
|
|
|
|
|
|
|
if (isBefore(startDate, start)) { |
|
|
|
|
|
startDate = start; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const rangeEndDateString = format(endDate, DATE_FORMAT); |
|
|
|
|
|
const rangeStartDateString = format(startDate, DATE_FORMAT); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const { |
|
|
|
|
|
dateRange, |
|
|
|
|
|
endDateString: rangeEndDateString, |
|
|
|
|
|
startDateString: rangeStartDateString |
|
|
|
|
|
} of dateRanges) { |
|
|
const currentValuesAtDateRangeStartWithCurrencyEffect = |
|
|
const currentValuesAtDateRangeStartWithCurrencyEffect = |
|
|
currentValuesWithCurrencyEffect[rangeStartDateString] ?? new Big(0); |
|
|
currentValuesWithCurrencyEffect[rangeStartDateString] ?? new Big(0); |
|
|
|
|
|
|
|
|
@ -667,27 +325,61 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|
|
items: averageInvestmentItems |
|
|
items: averageInvestmentItems |
|
|
}).at(-1); |
|
|
}).at(-1); |
|
|
|
|
|
|
|
|
const average = weightedInvestment |
|
|
averageInvestmentByDateRange[dateRange] = weightedInvestment |
|
|
? this.getAverageInvestment(weightedInvestment) |
|
|
? this.getAverageInvestment(weightedInvestment) |
|
|
: new Big(0); |
|
|
: new Big(0); |
|
|
|
|
|
|
|
|
netPerformanceWithCurrencyEffectMap[dateRange] = |
|
|
|
|
|
netPerformanceValuesWithCurrencyEffect[rangeEndDateString]?.minus( |
|
|
|
|
|
// If the date range is 'max', take 0 as a start value. Otherwise,
|
|
|
|
|
|
// the value of the end of the day of the start date is taken which
|
|
|
|
|
|
// differs from the buying price.
|
|
|
|
|
|
dateRange === 'max' |
|
|
|
|
|
? new Big(0) |
|
|
|
|
|
: (netPerformanceValuesWithCurrencyEffect[rangeStartDateString] ?? |
|
|
|
|
|
new Big(0)) |
|
|
|
|
|
) ?? new Big(0); |
|
|
|
|
|
|
|
|
|
|
|
netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0) |
|
|
|
|
|
? netPerformanceWithCurrencyEffectMap[dateRange].div(average) |
|
|
|
|
|
: new Big(0); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const holdingPerformanceWithAverageInvestment = |
|
|
|
|
|
this.getHoldingPerformanceWithInvestedCapital({ |
|
|
|
|
|
daysInMarket, |
|
|
|
|
|
holdingPerformance, |
|
|
|
|
|
investedCapital: averageInvestmentBetweenStartAndEndDate, |
|
|
|
|
|
investedCapitalByDateRange: averageInvestmentByDateRange, |
|
|
|
|
|
investedCapitalValues: averageInvestmentValues, |
|
|
|
|
|
investedCapitalValuesWithCurrencyEffect: |
|
|
|
|
|
averageInvestmentValuesWithCurrencyEffect, |
|
|
|
|
|
investedCapitalWithCurrencyEffect: |
|
|
|
|
|
averageInvestmentBetweenStartAndEndDateWithCurrencyEffect |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
if (PortfolioCalculator.ENABLE_LOGGING) { |
|
|
if (PortfolioCalculator.ENABLE_LOGGING) { |
|
|
|
|
|
const { activities, unitPriceAtEndDate } = valuation; |
|
|
|
|
|
|
|
|
|
|
|
const { |
|
|
|
|
|
fees: feesAtStartDate, |
|
|
|
|
|
feesWithCurrencyEffect: feesAtStartDateWithCurrencyEffect |
|
|
|
|
|
} = items[indexOfStartActivity]; |
|
|
|
|
|
|
|
|
|
|
|
const { |
|
|
|
|
|
fees, |
|
|
|
|
|
feesWithCurrencyEffect, |
|
|
|
|
|
quantity: totalQuantity |
|
|
|
|
|
} = items[indexOfEndActivity]; |
|
|
|
|
|
|
|
|
|
|
|
const { |
|
|
|
|
|
grossPerformance, |
|
|
|
|
|
grossPerformancePercentage, |
|
|
|
|
|
grossPerformancePercentageWithCurrencyEffect, |
|
|
|
|
|
grossPerformanceWithCurrencyEffect, |
|
|
|
|
|
netPerformance, |
|
|
|
|
|
netPerformancePercentage, |
|
|
|
|
|
netPerformancePercentageWithCurrencyEffectMap, |
|
|
|
|
|
totalDividend, |
|
|
|
|
|
totalInvestment, |
|
|
|
|
|
totalInvestmentWithCurrencyEffect |
|
|
|
|
|
} = holdingPerformanceWithAverageInvestment; |
|
|
|
|
|
|
|
|
|
|
|
const feesPerUnit = totalQuantity.gt(0) |
|
|
|
|
|
? fees.minus(feesAtStartDate).div(totalQuantity) |
|
|
|
|
|
: new Big(0); |
|
|
|
|
|
|
|
|
|
|
|
const feesPerUnitWithCurrencyEffect = totalQuantity.gt(0) |
|
|
|
|
|
? feesWithCurrencyEffect |
|
|
|
|
|
.minus(feesAtStartDateWithCurrencyEffect) |
|
|
|
|
|
.div(totalQuantity) |
|
|
|
|
|
: new Big(0); |
|
|
|
|
|
|
|
|
console.log( |
|
|
console.log( |
|
|
` |
|
|
` |
|
|
${symbol} |
|
|
${symbol} |
|
|
@ -705,10 +397,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|
|
2 |
|
|
2 |
|
|
)} |
|
|
)} |
|
|
Total dividend: ${totalDividend.toFixed(2)} |
|
|
Total dividend: ${totalDividend.toFixed(2)} |
|
|
Gross performance: ${totalGrossPerformance.toFixed( |
|
|
Gross performance: ${grossPerformance.toFixed( |
|
|
2 |
|
|
2 |
|
|
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}% |
|
|
)} / ${grossPerformancePercentage.mul(100).toFixed(2)}% |
|
|
Gross performance with currency effect: ${totalGrossPerformanceWithCurrencyEffect.toFixed( |
|
|
Gross performance with currency effect: ${grossPerformanceWithCurrencyEffect.toFixed( |
|
|
2 |
|
|
2 |
|
|
)} / ${grossPerformancePercentageWithCurrencyEffect |
|
|
)} / ${grossPerformancePercentageWithCurrencyEffect |
|
|
.mul(100) |
|
|
.mul(100) |
|
|
@ -717,7 +409,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|
|
Fees per unit with currency effect: ${feesPerUnitWithCurrencyEffect.toFixed( |
|
|
Fees per unit with currency effect: ${feesPerUnitWithCurrencyEffect.toFixed( |
|
|
2 |
|
|
2 |
|
|
)} |
|
|
)} |
|
|
Net performance: ${totalNetPerformance.toFixed( |
|
|
Net performance: ${netPerformance.toFixed( |
|
|
2 |
|
|
2 |
|
|
)} / ${netPerformancePercentage.mul(100).toFixed(2)}% |
|
|
)} / ${netPerformancePercentage.mul(100).toFixed(2)}% |
|
|
Net performance with currency effect: ${netPerformancePercentageWithCurrencyEffectMap[ |
|
|
Net performance with currency effect: ${netPerformancePercentageWithCurrencyEffectMap[ |
|
|
@ -726,39 +418,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return { |
|
|
return holdingPerformanceWithAverageInvestment; |
|
|
averageInvestmentValues, |
|
|
|
|
|
averageInvestmentValuesWithCurrencyEffect, |
|
|
|
|
|
currentValues, |
|
|
|
|
|
currentValuesWithCurrencyEffect, |
|
|
|
|
|
dividendValuesWithCurrencyEffect, |
|
|
|
|
|
dividendYieldPercent, |
|
|
|
|
|
dividendYieldPercentWithCurrencyEffect, |
|
|
|
|
|
grossPerformancePercentage, |
|
|
|
|
|
grossPerformancePercentageWithCurrencyEffect, |
|
|
|
|
|
investmentValuesAccumulated, |
|
|
|
|
|
investmentValuesAccumulatedWithCurrencyEffect, |
|
|
|
|
|
investmentValuesWithCurrencyEffect, |
|
|
|
|
|
netPerformancePercentage, |
|
|
|
|
|
netPerformancePercentageWithCurrencyEffectMap, |
|
|
|
|
|
netPerformanceValues, |
|
|
|
|
|
netPerformanceValuesWithCurrencyEffect, |
|
|
|
|
|
netPerformanceWithCurrencyEffectMap, |
|
|
|
|
|
totalDividend, |
|
|
|
|
|
totalDividendInBaseCurrency, |
|
|
|
|
|
totalInterestInBaseCurrency, |
|
|
|
|
|
totalInvestment, |
|
|
|
|
|
totalInvestmentWithCurrencyEffect, |
|
|
|
|
|
totalLiabilitiesInBaseCurrency, |
|
|
|
|
|
averageInvestment: averageInvestmentBetweenStartAndEndDate, |
|
|
|
|
|
averageInvestmentWithCurrencyEffect: |
|
|
|
|
|
averageInvestmentBetweenStartAndEndDateWithCurrencyEffect, |
|
|
|
|
|
grossPerformance: totalGrossPerformance, |
|
|
|
|
|
grossPerformanceWithCurrencyEffect: |
|
|
|
|
|
totalGrossPerformanceWithCurrencyEffect, |
|
|
|
|
|
hasErrors: totalQuantity.gt(0) && (!initialValue || !unitPriceAtEndDate), |
|
|
|
|
|
netPerformance: totalNetPerformance |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
protected getPerformanceCalculationType() { |
|
|
protected getPerformanceCalculationType() { |
|
|
|