Browse Source

Task/move holding valuation of portfolio calculator to base class (#7917)

Move holding valuation of portfolio calculator to base class
pull/7918/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
e55f9f0e43
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 369
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  2. 374
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  3. 24
      apps/api/src/app/portfolio/interfaces/holding-valuation-item.interface.ts
  4. 15
      apps/api/src/app/portfolio/interfaces/holding-valuation.interface.ts

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

@ -1,6 +1,8 @@
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { PortfolioSnapshotComputationError } from '@ghostfolio/api/app/portfolio/errors/portfolio-snapshot-computation.error';
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';
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 { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface';
@ -76,6 +78,7 @@ export abstract class PortfolioCalculator {
protected accountBalanceItems: HistoricalDataItem[];
protected activities: PortfolioCalculatorActivity[];
protected activitiesByAssetProfileIdentifier: {
[assetProfileIdentifier: string]: PortfolioCalculatorActivity[];
};
@ -918,6 +921,372 @@ export abstract class PortfolioCalculator {
start: Date;
} & AssetProfileIdentifier): HoldingPerformance;
protected getHoldingValuation({
activities,
exchangeRates,
unitPriceAtStartDate
}: {
activities: PortfolioCalculatorActivityItem[];
exchangeRates: { [dateString: string]: number };
unitPriceAtStartDate: Big | undefined;
}): HoldingValuation {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const currentValues: { [date: string]: Big } = {};
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
let fees = new Big(0);
let feesAtStartDate = new Big(0);
let feesAtStartDateWithCurrencyEffect = new Big(0);
let feesWithCurrencyEffect = new Big(0);
let grossPerformance = new Big(0);
let grossPerformanceWithCurrencyEffect = new Big(0);
let grossPerformanceAtStartDate = new Big(0);
let grossPerformanceAtStartDateWithCurrencyEffect = new Big(0);
let grossPerformanceFromSells = new Big(0);
let grossPerformanceFromSellsWithCurrencyEffect = new Big(0);
let initialValue: Big;
const investmentValuesAccumulated: { [date: string]: Big } = {};
const investmentValuesAccumulatedWithCurrencyEffect: {
[date: string]: Big;
} = {};
const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {};
const items: HoldingValuationItem[] = [];
let lastAveragePrice = new Big(0);
let lastAveragePriceWithCurrencyEffect = new Big(0);
const netPerformanceValues: { [date: string]: Big } = {};
const netPerformanceValuesWithCurrencyEffect: { [date: string]: Big } = {};
let totalInvestment = new Big(0);
let totalInvestmentFromBuyTransactions = new Big(0);
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
let totalInvestmentWithCurrencyEffect = new Big(0);
let totalQuantity = new Big(0);
let totalQuantityFromBuyTransactions = new Big(0);
const indexOfStartActivity = activities.findIndex(({ itemType }) => {
return itemType === 'start';
});
const indexOfEndActivity = activities.findIndex(({ itemType }) => {
return itemType === 'end';
});
for (let i = 0; i < activities.length; i += 1) {
const activity = activities[i];
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log();
console.log();
console.log(
i + 1,
activity.date,
activity.type,
activity.itemType ? `(${activity.itemType})` : ''
);
}
const exchangeRateAtActivityDate = exchangeRates[activity.date];
if (activity.itemType === 'start') {
// Take the unit price of the activity as the market price if there are no
// activities of this symbol before the start date
activity.unitPrice =
indexOfStartActivity === 0
? activities[i + 1]?.unitPrice
: unitPriceAtStartDate;
}
if (activity.fee) {
activity.feeInBaseCurrency = activity.fee.mul(currentExchangeRate ?? 1);
activity.feeInBaseCurrencyWithCurrencyEffect = activity.fee.mul(
exchangeRateAtActivityDate ?? 1
);
}
const unitPrice = ['BUY', 'SELL'].includes(activity.type)
? activity.unitPrice
: activity.unitPriceFromMarketData;
if (unitPrice) {
activity.unitPriceInBaseCurrency = unitPrice.mul(
currentExchangeRate ?? 1
);
activity.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul(
exchangeRateAtActivityDate ?? 1
);
}
const marketPriceInBaseCurrency =
activity.unitPriceFromMarketData?.mul(currentExchangeRate ?? 1) ??
new Big(0);
const marketPriceInBaseCurrencyWithCurrencyEffect =
activity.unitPriceFromMarketData?.mul(
exchangeRateAtActivityDate ?? 1
) ?? new Big(0);
const valueOfInvestmentBeforeTransaction = totalQuantity.mul(
marketPriceInBaseCurrency
);
const valueOfInvestmentBeforeTransactionWithCurrencyEffect =
totalQuantity.mul(marketPriceInBaseCurrencyWithCurrencyEffect);
let transactionInvestment = new Big(0);
let transactionInvestmentWithCurrencyEffect = new Big(0);
if (activity.type === 'BUY') {
transactionInvestment = activity.quantity
.mul(activity.unitPriceInBaseCurrency)
.mul(getFactor(activity.type));
transactionInvestmentWithCurrencyEffect = activity.quantity
.mul(activity.unitPriceInBaseCurrencyWithCurrencyEffect)
.mul(getFactor(activity.type));
totalQuantityFromBuyTransactions =
totalQuantityFromBuyTransactions.plus(activity.quantity);
totalInvestmentFromBuyTransactions =
totalInvestmentFromBuyTransactions.plus(transactionInvestment);
totalInvestmentFromBuyTransactionsWithCurrencyEffect =
totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus(
transactionInvestmentWithCurrencyEffect
);
} else if (activity.type === 'SELL') {
if (totalQuantity.gt(0)) {
const remainingQuantity = totalQuantity.minus(activity.quantity);
transactionInvestment = totalInvestment
.mul(remainingQuantity)
.div(totalQuantity)
.minus(totalInvestment);
transactionInvestmentWithCurrencyEffect =
totalInvestmentWithCurrencyEffect
.mul(remainingQuantity)
.div(totalQuantity)
.minus(totalInvestmentWithCurrencyEffect);
}
}
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log('activity.quantity', activity.quantity.toNumber());
console.log('transactionInvestment', transactionInvestment.toNumber());
console.log(
'transactionInvestmentWithCurrencyEffect',
transactionInvestmentWithCurrencyEffect.toNumber()
);
}
const totalInvestmentBeforeTransaction = totalInvestment;
const totalInvestmentBeforeTransactionWithCurrencyEffect =
totalInvestmentWithCurrencyEffect;
totalInvestment = totalInvestment.plus(transactionInvestment);
totalInvestmentWithCurrencyEffect =
totalInvestmentWithCurrencyEffect.plus(
transactionInvestmentWithCurrencyEffect
);
if (i >= indexOfStartActivity && !initialValue) {
if (
i === indexOfStartActivity &&
!valueOfInvestmentBeforeTransaction.eq(0)
) {
initialValue = valueOfInvestmentBeforeTransaction;
} else if (transactionInvestment.gt(0)) {
initialValue = transactionInvestment;
}
}
fees = fees.plus(activity.feeInBaseCurrency ?? 0);
feesWithCurrencyEffect = feesWithCurrencyEffect.plus(
activity.feeInBaseCurrencyWithCurrencyEffect ?? 0
);
totalQuantity = totalQuantity.plus(
activity.quantity.mul(getFactor(activity.type))
);
const valueOfInvestment = totalQuantity.mul(marketPriceInBaseCurrency);
const valueOfInvestmentWithCurrencyEffect = totalQuantity.mul(
marketPriceInBaseCurrencyWithCurrencyEffect
);
const grossPerformanceFromSell =
activity.type === 'SELL'
? activity.unitPriceInBaseCurrency
.minus(lastAveragePrice)
.mul(activity.quantity)
: new Big(0);
const grossPerformanceFromSellWithCurrencyEffect =
activity.type === 'SELL'
? activity.unitPriceInBaseCurrencyWithCurrencyEffect
.minus(lastAveragePriceWithCurrencyEffect)
.mul(activity.quantity)
: new Big(0);
grossPerformanceFromSells = grossPerformanceFromSells.plus(
grossPerformanceFromSell
);
grossPerformanceFromSellsWithCurrencyEffect =
grossPerformanceFromSellsWithCurrencyEffect.plus(
grossPerformanceFromSellWithCurrencyEffect
);
lastAveragePrice = totalQuantityFromBuyTransactions.eq(0)
? new Big(0)
: totalInvestmentFromBuyTransactions.div(
totalQuantityFromBuyTransactions
);
lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq(
0
)
? new Big(0)
: totalInvestmentFromBuyTransactionsWithCurrencyEffect.div(
totalQuantityFromBuyTransactions
);
if (totalQuantity.eq(0)) {
// Reset tracking variables when position is fully closed
totalInvestmentFromBuyTransactions = new Big(0);
totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
totalQuantityFromBuyTransactions = new Big(0);
}
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log(
'grossPerformanceFromSells',
grossPerformanceFromSells.toNumber()
);
console.log(
'grossPerformanceFromSellWithCurrencyEffect',
grossPerformanceFromSellWithCurrencyEffect.toNumber()
);
}
const newGrossPerformance = valueOfInvestment
.minus(totalInvestment)
.plus(grossPerformanceFromSells);
const newGrossPerformanceWithCurrencyEffect =
valueOfInvestmentWithCurrencyEffect
.minus(totalInvestmentWithCurrencyEffect)
.plus(grossPerformanceFromSellsWithCurrencyEffect);
grossPerformance = newGrossPerformance;
grossPerformanceWithCurrencyEffect =
newGrossPerformanceWithCurrencyEffect;
if (activity.itemType === 'start') {
feesAtStartDate = fees;
feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect;
grossPerformanceAtStartDate = grossPerformance;
grossPerformanceAtStartDateWithCurrencyEffect =
grossPerformanceWithCurrencyEffect;
}
if (i > indexOfStartActivity) {
currentValues[activity.date] = valueOfInvestment;
currentValuesWithCurrencyEffect[activity.date] =
valueOfInvestmentWithCurrencyEffect;
netPerformanceValues[activity.date] = grossPerformance
.minus(grossPerformanceAtStartDate)
.minus(fees.minus(feesAtStartDate));
netPerformanceValuesWithCurrencyEffect[activity.date] =
grossPerformanceWithCurrencyEffect
.minus(grossPerformanceAtStartDateWithCurrencyEffect)
.minus(
feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect)
);
investmentValuesAccumulated[activity.date] = totalInvestment;
investmentValuesAccumulatedWithCurrencyEffect[activity.date] =
totalInvestmentWithCurrencyEffect;
investmentValuesWithCurrencyEffect[activity.date] = (
investmentValuesWithCurrencyEffect[activity.date] ?? new Big(0)
).add(transactionInvestmentWithCurrencyEffect);
}
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log('totalInvestment', totalInvestment.toNumber());
console.log(
'totalInvestmentWithCurrencyEffect',
totalInvestmentWithCurrencyEffect.toNumber()
);
console.log(
'totalGrossPerformance',
grossPerformance.minus(grossPerformanceAtStartDate).toNumber()
);
console.log(
'totalGrossPerformanceWithCurrencyEffect',
grossPerformanceWithCurrencyEffect
.minus(grossPerformanceAtStartDateWithCurrencyEffect)
.toNumber()
);
}
items.push({
fees,
feesWithCurrencyEffect,
grossPerformance,
grossPerformanceWithCurrencyEffect,
transactionInvestment,
transactionInvestmentWithCurrencyEffect,
date: activity.date,
investment: totalInvestment,
investmentBeforeTransaction: totalInvestmentBeforeTransaction,
investmentBeforeTransactionWithCurrencyEffect:
totalInvestmentBeforeTransactionWithCurrencyEffect,
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
itemType: activity.itemType,
quantity: totalQuantity,
type: activity.type,
value: valueOfInvestment,
valueBeforeTransaction: valueOfInvestmentBeforeTransaction,
valueBeforeTransactionWithCurrencyEffect:
valueOfInvestmentBeforeTransactionWithCurrencyEffect,
valueWithCurrencyEffect: valueOfInvestmentWithCurrencyEffect
});
if (i === indexOfEndActivity) {
break;
}
}
return {
currentValues,
currentValuesWithCurrencyEffect,
initialValue,
investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect,
items,
netPerformanceValues,
netPerformanceValuesWithCurrencyEffect
};
}
public async getInterestInBaseCurrency() {
await this.snapshotPromise;

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

@ -2,7 +2,6 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/po
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 { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import {
DATE_FORMAT,
@ -138,43 +137,14 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
};
start: Date;
} & AssetProfileIdentifier): HoldingPerformance {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const currentValues: { [date: string]: Big } = {};
const currentValuesWithCurrencyEffect: { [date: string]: Big } = {};
let fees = new Big(0);
let feesAtStartDate = new Big(0);
let feesAtStartDateWithCurrencyEffect = new Big(0);
let feesWithCurrencyEffect = new Big(0);
let grossPerformance = new Big(0);
let grossPerformanceWithCurrencyEffect = new Big(0);
let grossPerformanceAtStartDate = new Big(0);
let grossPerformanceAtStartDateWithCurrencyEffect = new Big(0);
let grossPerformanceFromSells = new Big(0);
let grossPerformanceFromSellsWithCurrencyEffect = new Big(0);
let initialValue: Big;
let investmentAtStartDate: Big;
let investmentAtStartDateWithCurrencyEffect: Big;
const investmentValuesAccumulated: { [date: string]: Big } = {};
const investmentValuesAccumulatedWithCurrencyEffect: {
[date: string]: Big;
} = {};
const investmentValuesWithCurrencyEffect: { [date: string]: Big } = {};
let lastAveragePrice = new Big(0);
let lastAveragePriceWithCurrencyEffect = new Big(0);
const netPerformanceValues: { [date: string]: Big } = {};
const netPerformanceValuesWithCurrencyEffect: { [date: string]: Big } = {};
const timeWeightedInvestmentValues: { [date: string]: Big } = {};
const timeWeightedInvestmentValuesWithCurrencyEffect: {
[date: string]: Big;
} = {};
let totalInvestment = new Big(0);
let totalInvestmentFromBuyTransactions = new Big(0);
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
let totalInvestmentWithCurrencyEffect = new Big(0);
let totalQuantity = new Big(0);
let totalQuantityFromBuyTransactions = new Big(0);
let valueAtStartDate: Big;
let valueAtStartDateWithCurrencyEffect: Big;
@ -254,11 +224,27 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}
});
const indexOfStartActivity = activities.findIndex(({ itemType }) => {
const {
currentValues,
currentValuesWithCurrencyEffect,
initialValue,
investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect,
items,
netPerformanceValues,
netPerformanceValuesWithCurrencyEffect
} = this.getHoldingValuation({
activities,
exchangeRates,
unitPriceAtStartDate
});
const indexOfStartActivity = items.findIndex(({ itemType }) => {
return itemType === 'start';
});
const indexOfEndActivity = activities.findIndex(({ itemType }) => {
const indexOfEndActivity = items.findIndex(({ itemType }) => {
return itemType === 'end';
});
@ -266,255 +252,31 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let sumOfTimeWeightedInvestments = new Big(0);
let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0);
for (let i = 0; i < activities.length; i += 1) {
const activity = activities[i];
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log();
console.log();
console.log(
i + 1,
activity.date,
activity.type,
activity.itemType ? `(${activity.itemType})` : ''
);
}
const exchangeRateAtActivityDate = exchangeRates[activity.date];
if (activity.itemType === 'start') {
// Take the unit price of the activity as the market price if there are no
// activities of this symbol before the start date
activity.unitPrice =
indexOfStartActivity === 0
? activities[i + 1]?.unitPrice
: unitPriceAtStartDate;
}
if (activity.fee) {
activity.feeInBaseCurrency = activity.fee.mul(currentExchangeRate ?? 1);
activity.feeInBaseCurrencyWithCurrencyEffect = activity.fee.mul(
exchangeRateAtActivityDate ?? 1
);
}
const unitPrice = ['BUY', 'SELL'].includes(activity.type)
? activity.unitPrice
: activity.unitPriceFromMarketData;
if (unitPrice) {
activity.unitPriceInBaseCurrency = unitPrice.mul(
currentExchangeRate ?? 1
);
activity.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul(
exchangeRateAtActivityDate ?? 1
);
}
const marketPriceInBaseCurrency =
activity.unitPriceFromMarketData?.mul(currentExchangeRate ?? 1) ??
new Big(0);
const marketPriceInBaseCurrencyWithCurrencyEffect =
activity.unitPriceFromMarketData?.mul(
exchangeRateAtActivityDate ?? 1
) ?? new Big(0);
const valueOfInvestmentBeforeTransaction = totalQuantity.mul(
marketPriceInBaseCurrency
);
const valueOfInvestmentBeforeTransactionWithCurrencyEffect =
totalQuantity.mul(marketPriceInBaseCurrencyWithCurrencyEffect);
for (let i = 0; i < items.length; i += 1) {
const item = items[i];
if (!investmentAtStartDate && i >= indexOfStartActivity) {
investmentAtStartDate = totalInvestment ?? new Big(0);
investmentAtStartDate = item.investmentBeforeTransaction;
investmentAtStartDateWithCurrencyEffect =
totalInvestmentWithCurrencyEffect ?? new Big(0);
item.investmentBeforeTransactionWithCurrencyEffect;
valueAtStartDate = valueOfInvestmentBeforeTransaction;
valueAtStartDate = item.valueBeforeTransaction;
valueAtStartDateWithCurrencyEffect =
valueOfInvestmentBeforeTransactionWithCurrencyEffect;
}
let transactionInvestment = new Big(0);
let transactionInvestmentWithCurrencyEffect = new Big(0);
if (activity.type === 'BUY') {
transactionInvestment = activity.quantity
.mul(activity.unitPriceInBaseCurrency)
.mul(getFactor(activity.type));
transactionInvestmentWithCurrencyEffect = activity.quantity
.mul(activity.unitPriceInBaseCurrencyWithCurrencyEffect)
.mul(getFactor(activity.type));
totalQuantityFromBuyTransactions =
totalQuantityFromBuyTransactions.plus(activity.quantity);
totalInvestmentFromBuyTransactions =
totalInvestmentFromBuyTransactions.plus(transactionInvestment);
totalInvestmentFromBuyTransactionsWithCurrencyEffect =
totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus(
transactionInvestmentWithCurrencyEffect
);
} else if (activity.type === 'SELL') {
if (totalQuantity.gt(0)) {
const remainingQuantity = totalQuantity.minus(activity.quantity);
transactionInvestment = totalInvestment
.mul(remainingQuantity)
.div(totalQuantity)
.minus(totalInvestment);
transactionInvestmentWithCurrencyEffect =
totalInvestmentWithCurrencyEffect
.mul(remainingQuantity)
.div(totalQuantity)
.minus(totalInvestmentWithCurrencyEffect);
}
}
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log('activity.quantity', activity.quantity.toNumber());
console.log('transactionInvestment', transactionInvestment.toNumber());
console.log(
'transactionInvestmentWithCurrencyEffect',
transactionInvestmentWithCurrencyEffect.toNumber()
);
}
const totalInvestmentBeforeTransaction = totalInvestment;
const totalInvestmentBeforeTransactionWithCurrencyEffect =
totalInvestmentWithCurrencyEffect;
totalInvestment = totalInvestment.plus(transactionInvestment);
totalInvestmentWithCurrencyEffect =
totalInvestmentWithCurrencyEffect.plus(
transactionInvestmentWithCurrencyEffect
);
if (i >= indexOfStartActivity && !initialValue) {
if (
i === indexOfStartActivity &&
!valueOfInvestmentBeforeTransaction.eq(0)
) {
initialValue = valueOfInvestmentBeforeTransaction;
} else if (transactionInvestment.gt(0)) {
initialValue = transactionInvestment;
}
}
fees = fees.plus(activity.feeInBaseCurrency ?? 0);
feesWithCurrencyEffect = feesWithCurrencyEffect.plus(
activity.feeInBaseCurrencyWithCurrencyEffect ?? 0
);
totalQuantity = totalQuantity.plus(
activity.quantity.mul(getFactor(activity.type))
);
const valueOfInvestment = totalQuantity.mul(marketPriceInBaseCurrency);
const valueOfInvestmentWithCurrencyEffect = totalQuantity.mul(
marketPriceInBaseCurrencyWithCurrencyEffect
);
const grossPerformanceFromSell =
activity.type === 'SELL'
? activity.unitPriceInBaseCurrency
.minus(lastAveragePrice)
.mul(activity.quantity)
: new Big(0);
const grossPerformanceFromSellWithCurrencyEffect =
activity.type === 'SELL'
? activity.unitPriceInBaseCurrencyWithCurrencyEffect
.minus(lastAveragePriceWithCurrencyEffect)
.mul(activity.quantity)
: new Big(0);
grossPerformanceFromSells = grossPerformanceFromSells.plus(
grossPerformanceFromSell
);
grossPerformanceFromSellsWithCurrencyEffect =
grossPerformanceFromSellsWithCurrencyEffect.plus(
grossPerformanceFromSellWithCurrencyEffect
);
lastAveragePrice = totalQuantityFromBuyTransactions.eq(0)
? new Big(0)
: totalInvestmentFromBuyTransactions.div(
totalQuantityFromBuyTransactions
);
lastAveragePriceWithCurrencyEffect = totalQuantityFromBuyTransactions.eq(
0
)
? new Big(0)
: totalInvestmentFromBuyTransactionsWithCurrencyEffect.div(
totalQuantityFromBuyTransactions
);
if (totalQuantity.eq(0)) {
// Reset tracking variables when position is fully closed
totalInvestmentFromBuyTransactions = new Big(0);
totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
totalQuantityFromBuyTransactions = new Big(0);
}
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log(
'grossPerformanceFromSells',
grossPerformanceFromSells.toNumber()
);
console.log(
'grossPerformanceFromSellWithCurrencyEffect',
grossPerformanceFromSellWithCurrencyEffect.toNumber()
);
}
const newGrossPerformance = valueOfInvestment
.minus(totalInvestment)
.plus(grossPerformanceFromSells);
const newGrossPerformanceWithCurrencyEffect =
valueOfInvestmentWithCurrencyEffect
.minus(totalInvestmentWithCurrencyEffect)
.plus(grossPerformanceFromSellsWithCurrencyEffect);
grossPerformance = newGrossPerformance;
grossPerformanceWithCurrencyEffect =
newGrossPerformanceWithCurrencyEffect;
if (activity.itemType === 'start') {
feesAtStartDate = fees;
feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect;
grossPerformanceAtStartDate = grossPerformance;
grossPerformanceAtStartDateWithCurrencyEffect =
grossPerformanceWithCurrencyEffect;
item.valueBeforeTransactionWithCurrencyEffect;
}
if (i > indexOfStartActivity) {
// Only consider periods with an investment for the calculation of
// the time weighted investment
if (
valueOfInvestmentBeforeTransaction.gt(0) &&
['BUY', 'SELL'].includes(activity.type)
item.valueBeforeTransaction.gt(0) &&
['BUY', 'SELL'].includes(item.type)
) {
// Calculate the number of days since the previous activity
const activityDate = new Date(activity.date);
const previousActivityDate = new Date(activities[i - 1].date);
const activityDate = new Date(item.date);
const previousActivityDate = new Date(items[i - 1].date);
let daysSinceLastActivity = differenceInDays(
activityDate,
@ -533,7 +295,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add(
valueAtStartDate
.minus(investmentAtStartDate)
.plus(totalInvestmentBeforeTransaction)
.plus(item.investmentBeforeTransaction)
.mul(daysSinceLastActivity)
);
@ -541,81 +303,53 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
sumOfTimeWeightedInvestmentsWithCurrencyEffect.add(
valueAtStartDateWithCurrencyEffect
.minus(investmentAtStartDateWithCurrencyEffect)
.plus(totalInvestmentBeforeTransactionWithCurrencyEffect)
.plus(item.investmentBeforeTransactionWithCurrencyEffect)
.mul(daysSinceLastActivity)
);
}
currentValues[activity.date] = valueOfInvestment;
currentValuesWithCurrencyEffect[activity.date] =
valueOfInvestmentWithCurrencyEffect;
netPerformanceValues[activity.date] = grossPerformance
.minus(grossPerformanceAtStartDate)
.minus(fees.minus(feesAtStartDate));
netPerformanceValuesWithCurrencyEffect[activity.date] =
grossPerformanceWithCurrencyEffect
.minus(grossPerformanceAtStartDateWithCurrencyEffect)
.minus(
feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect)
);
investmentValuesAccumulated[activity.date] = totalInvestment;
investmentValuesAccumulatedWithCurrencyEffect[activity.date] =
totalInvestmentWithCurrencyEffect;
investmentValuesWithCurrencyEffect[activity.date] = (
investmentValuesWithCurrencyEffect[activity.date] ?? new Big(0)
).add(transactionInvestmentWithCurrencyEffect);
// If duration is effectively zero (first day), use the actual investment as the base.
// Otherwise, use the calculated time-weighted average.
timeWeightedInvestmentValues[activity.date] =
timeWeightedInvestmentValues[item.date] =
totalInvestmentDays > Number.EPSILON
? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
: totalInvestment.gt(0)
? totalInvestment
: item.investment.gt(0)
? item.investment
: new Big(0);
timeWeightedInvestmentValuesWithCurrencyEffect[activity.date] =
timeWeightedInvestmentValuesWithCurrencyEffect[item.date] =
totalInvestmentDays > Number.EPSILON
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div(
totalInvestmentDays
)
: totalInvestmentWithCurrencyEffect.gt(0)
? totalInvestmentWithCurrencyEffect
: item.investmentWithCurrencyEffect.gt(0)
? item.investmentWithCurrencyEffect
: new Big(0);
}
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log('totalInvestment', totalInvestment.toNumber());
console.log(
'totalInvestmentWithCurrencyEffect',
totalInvestmentWithCurrencyEffect.toNumber()
);
console.log(
'totalGrossPerformance',
grossPerformance.minus(grossPerformanceAtStartDate).toNumber()
);
console.log(
'totalGrossPerformanceWithCurrencyEffect',
grossPerformanceWithCurrencyEffect
.minus(grossPerformanceAtStartDateWithCurrencyEffect)
.toNumber()
);
}
if (i === indexOfEndActivity) {
break;
}
}
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
);

24
apps/api/src/app/portfolio/interfaces/holding-valuation-item.interface.ts

@ -0,0 +1,24 @@
import { Big } from 'big.js';
import { PortfolioCalculatorActivityItem } from './portfolio-calculator-activity-item.interface';
export interface HoldingValuationItem extends Pick<
PortfolioCalculatorActivityItem,
'date' | 'itemType' | 'type'
> {
fees: Big;
feesWithCurrencyEffect: Big;
grossPerformance: Big;
grossPerformanceWithCurrencyEffect: Big;
investment: Big;
investmentBeforeTransaction: Big;
investmentBeforeTransactionWithCurrencyEffect: Big;
investmentWithCurrencyEffect: Big;
quantity: Big;
transactionInvestment: Big;
transactionInvestmentWithCurrencyEffect: Big;
value: Big;
valueBeforeTransaction: Big;
valueBeforeTransactionWithCurrencyEffect: Big;
valueWithCurrencyEffect: Big;
}

15
apps/api/src/app/portfolio/interfaces/holding-valuation.interface.ts

@ -0,0 +1,15 @@
import { Big } from 'big.js';
import { HoldingValuationItem } from './holding-valuation-item.interface';
export interface HoldingValuation {
currentValues: { [date: string]: Big };
currentValuesWithCurrencyEffect: { [date: string]: Big };
initialValue?: Big;
investmentValuesAccumulated: { [date: string]: Big };
investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big };
investmentValuesWithCurrencyEffect: { [date: string]: Big };
items: HoldingValuationItem[];
netPerformanceValues: { [date: string]: Big };
netPerformanceValuesWithCurrencyEffect: { [date: string]: Big };
}
Loading…
Cancel
Save