Browse Source

Task/rename order to activity in portfolio calculator (#7916)

Rename order to activity
pull/7918/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
adae69b065
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 308
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  2. 175
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  3. 4
      apps/api/src/app/portfolio/interfaces/portfolio-calculator-activity-item.interface.ts
  4. 5
      apps/api/src/app/portfolio/interfaces/portfolio-calculator-activity.interface.ts

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

@ -1,9 +1,9 @@
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 { 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';
import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface';
import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface';
import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface';
@ -75,9 +75,9 @@ export abstract class PortfolioCalculator {
protected readonly logger = new Logger(PortfolioCalculator.name);
protected accountBalanceItems: HistoricalDataItem[];
protected activities: PortfolioOrder[];
protected activities: PortfolioCalculatorActivity[];
protected activitiesByAssetProfileIdentifier: {
[assetProfileIdentifier: string]: PortfolioOrder[];
[assetProfileIdentifier: string]: PortfolioCalculatorActivity[];
};
private configurationService: ConfigurationService;
@ -716,6 +716,139 @@ export abstract class PortfolioCalculator {
};
}
protected getActivitiesWithMarketPrices({
activities,
assetProfile,
chartDates,
endDateString,
marketSymbolMap,
startDateString,
unitPriceAtEndDate,
unitPriceAtStartDate
}: {
activities: PortfolioCalculatorActivityItem[];
assetProfile: PortfolioCalculatorActivityItem['assetProfile'];
chartDates: string[];
endDateString: string;
marketSymbolMap: {
[date: string]: { [assetProfileIdentifier: string]: Big };
};
startDateString: string;
unitPriceAtEndDate: Big;
unitPriceAtStartDate: Big;
}): PortfolioCalculatorActivityItem[] {
if (activities.length <= 0) {
return [];
}
const assetProfileIdentifier = getAssetProfileIdentifier(assetProfile);
const dateStringOfFirstActivity = activities[0].date;
// Copy the items as they are enriched below. A shallow copy is sufficient
// because only top-level properties are written.
const activitiesWithMarketPrices = activities.map((activity) => {
return { ...activity };
});
// Add a synthetic activity at the start and the end date
activitiesWithMarketPrices.push({
assetProfile,
date: startDateString,
fee: new Big(0),
feeInBaseCurrency: new Big(0),
itemType: 'start',
quantity: new Big(0),
type: 'BUY',
unitPrice: unitPriceAtStartDate
});
activitiesWithMarketPrices.push({
assetProfile,
date: endDateString,
fee: new Big(0),
feeInBaseCurrency: new Big(0),
itemType: 'end',
quantity: new Big(0),
type: 'BUY',
unitPrice: unitPriceAtEndDate
});
// Fall back to the unit price of the most recent BUY / SELL activity for
// the chart dates before the first known market price of the symbol
let lastActivityUnitPrice: Big | undefined;
let lastMarketPrice: Big | undefined;
const activitiesByDate: {
[date: string]: PortfolioCalculatorActivityItem[];
} = {};
for (const activity of activitiesWithMarketPrices) {
activitiesByDate[activity.date] = activitiesByDate[activity.date] ?? [];
activitiesByDate[activity.date].push(activity);
}
for (const dateString of chartDates) {
if (dateString < startDateString) {
continue;
} else if (dateString > endDateString) {
break;
}
const activitiesOfDate = activitiesByDate[dateString];
if (!lastMarketPrice && activitiesOfDate?.length > 0) {
for (const { itemType, type, unitPrice } of activitiesOfDate) {
if (!itemType && ['BUY', 'SELL'].includes(type)) {
lastActivityUnitPrice = unitPrice;
}
}
}
const marketPrice = marketSymbolMap[dateString]?.[assetProfileIdentifier];
const unitPrice =
marketPrice ??
lastMarketPrice ??
lastActivityUnitPrice ??
unitPriceAtEndDate;
if (activitiesOfDate?.length > 0) {
for (const activity of activitiesOfDate) {
activity.unitPriceFromMarketData = unitPrice;
}
} else if (dateString >= dateStringOfFirstActivity) {
activitiesWithMarketPrices.push({
assetProfile,
unitPrice,
date: dateString,
fee: new Big(0),
feeInBaseCurrency: new Big(0),
quantity: new Big(0),
type: 'BUY',
unitPriceFromMarketData: unitPrice
});
}
if (marketPrice) {
lastMarketPrice = marketPrice;
}
}
// Sort the activities so that the start and end placeholder activities
// are at the correct position
return sortBy(activitiesWithMarketPrices, ({ date, itemType }) => {
let sortIndex = new Date(date);
if (itemType === 'end') {
sortIndex = addMilliseconds(sortIndex, 1);
} else if (itemType === 'start') {
sortIndex = addMilliseconds(sortIndex, -1);
}
return sortIndex.getTime();
});
}
public getDataProviderInfos() {
return this.dataProviderInfos;
}
@ -837,137 +970,6 @@ export abstract class PortfolioCalculator {
return this.snapshot.totalLiabilitiesWithCurrencyEffect;
}
protected getOrdersWithMarketPrices({
assetProfile,
chartDates,
endDateString,
marketSymbolMap,
orders,
startDateString,
unitPriceAtEndDate,
unitPriceAtStartDate
}: {
assetProfile: PortfolioOrderItem['assetProfile'];
chartDates: string[];
endDateString: string;
marketSymbolMap: {
[date: string]: { [assetProfileIdentifier: string]: Big };
};
orders: PortfolioOrderItem[];
startDateString: string;
unitPriceAtEndDate: Big;
unitPriceAtStartDate: Big;
}): PortfolioOrderItem[] {
if (orders.length <= 0) {
return [];
}
const assetProfileIdentifier = getAssetProfileIdentifier(assetProfile);
const dateStringOfFirstActivity = orders[0].date;
// Copy the items as they are enriched below. A shallow copy is sufficient
// because only top-level properties are written.
const ordersWithMarketPrices = orders.map((order) => {
return { ...order };
});
// Add a synthetic order at the start and the end date
ordersWithMarketPrices.push({
assetProfile,
date: startDateString,
fee: new Big(0),
feeInBaseCurrency: new Big(0),
itemType: 'start',
quantity: new Big(0),
type: 'BUY',
unitPrice: unitPriceAtStartDate
});
ordersWithMarketPrices.push({
assetProfile,
date: endDateString,
fee: new Big(0),
feeInBaseCurrency: new Big(0),
itemType: 'end',
quantity: new Big(0),
type: 'BUY',
unitPrice: unitPriceAtEndDate
});
// Fall back to the unit price of the most recent BUY / SELL activity for
// the chart dates before the first known market price of the symbol
let lastActivityUnitPrice: Big | undefined;
let lastMarketPrice: Big | undefined;
const ordersByDate: { [date: string]: PortfolioOrderItem[] } = {};
for (const order of ordersWithMarketPrices) {
ordersByDate[order.date] = ordersByDate[order.date] ?? [];
ordersByDate[order.date].push(order);
}
for (const dateString of chartDates) {
if (dateString < startDateString) {
continue;
} else if (dateString > endDateString) {
break;
}
const ordersOfDate = ordersByDate[dateString];
if (!lastMarketPrice && ordersOfDate?.length > 0) {
for (const { itemType, type, unitPrice } of ordersOfDate) {
if (!itemType && ['BUY', 'SELL'].includes(type)) {
lastActivityUnitPrice = unitPrice;
}
}
}
const marketPrice = marketSymbolMap[dateString]?.[assetProfileIdentifier];
const unitPrice =
marketPrice ??
lastMarketPrice ??
lastActivityUnitPrice ??
unitPriceAtEndDate;
if (ordersOfDate?.length > 0) {
for (const order of ordersOfDate) {
order.unitPriceFromMarketData = unitPrice;
}
} else if (dateString >= dateStringOfFirstActivity) {
ordersWithMarketPrices.push({
assetProfile,
unitPrice,
date: dateString,
fee: new Big(0),
feeInBaseCurrency: new Big(0),
quantity: new Big(0),
type: 'BUY',
unitPriceFromMarketData: unitPrice
});
}
if (marketPrice) {
lastMarketPrice = marketPrice;
}
}
// Sort orders so that the start and end placeholder order are at the correct
// position
return sortBy(ordersWithMarketPrices, ({ date, itemType }) => {
let sortIndex = new Date(date);
if (itemType === 'end') {
sortIndex = addMilliseconds(sortIndex, 1);
} else if (itemType === 'start') {
sortIndex = addMilliseconds(sortIndex, -1);
}
return sortIndex.getTime();
});
}
public async getPerformance({ end, start }) {
await this.snapshotPromise;
@ -1104,38 +1106,38 @@ export abstract class PortfolioCalculator {
}
protected getTotalsFromActivities({
exchangeRates,
orders
activities,
exchangeRates
}: {
activities: PortfolioCalculatorActivity[];
exchangeRates: { [dateString: string]: number };
orders: PortfolioOrder[];
}) {
let totalDividend = new Big(0);
let totalDividendInBaseCurrency = new Big(0);
let totalInterestInBaseCurrency = new Big(0);
let totalLiabilitiesInBaseCurrency = new Big(0);
for (const order of orders) {
const exchangeRateAtOrderDate = exchangeRates[order.date];
for (const activity of activities) {
const exchangeRateAtActivityDate = exchangeRates[activity.date];
if (order.type === 'DIVIDEND') {
const dividend = order.quantity.mul(order.unitPrice);
if (activity.type === 'DIVIDEND') {
const dividend = activity.quantity.mul(activity.unitPrice);
totalDividend = totalDividend.plus(dividend);
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
dividend.mul(exchangeRateAtOrderDate ?? 1)
dividend.mul(exchangeRateAtActivityDate ?? 1)
);
} else if (order.type === 'INTEREST') {
const interest = order.quantity.mul(order.unitPrice);
} else if (activity.type === 'INTEREST') {
const interest = activity.quantity.mul(activity.unitPrice);
totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus(
interest.mul(exchangeRateAtOrderDate ?? 1)
interest.mul(exchangeRateAtActivityDate ?? 1)
);
} else if (order.type === 'LIABILITY') {
const liabilities = order.quantity.mul(order.unitPrice);
} else if (activity.type === 'LIABILITY') {
const liabilities = activity.quantity.mul(activity.unitPrice);
totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus(
liabilities.mul(exchangeRateAtOrderDate ?? 1)
liabilities.mul(exchangeRateAtActivityDate ?? 1)
);
}
}
@ -1153,17 +1155,17 @@ export abstract class PortfolioCalculator {
}
protected getUnitPriceAtEndDate({
activities,
dataSource,
isCash,
marketPriceAtEndDate,
orders
marketPriceAtEndDate
}: {
activities: PortfolioCalculatorActivity[];
dataSource: DataSource;
isCash: boolean;
marketPriceAtEndDate: Big;
orders: PortfolioOrder[];
}): Big {
const latestActivity = orders.at(-1);
const latestActivity = activities.at(-1);
if (
dataSource === 'MANUAL' &&

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

@ -1,7 +1,7 @@
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
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 { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import {
@ -183,12 +183,12 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
symbol
});
let orders: PortfolioOrderItem[] =
let activities: PortfolioCalculatorActivityItem[] =
this.activitiesByAssetProfileIdentifier[assetProfileIdentifier] ?? [];
const isCash = orders[0]?.assetProfile?.assetSubClass === 'CASH';
const isCash = activities[0]?.assetProfile?.assetSubClass === 'CASH';
if (orders.length <= 0) {
if (activities.length <= 0) {
return this.getEmptyHoldingPerformance();
}
@ -200,9 +200,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
totalDividendInBaseCurrency,
totalInterestInBaseCurrency,
totalLiabilitiesInBaseCurrency
} = this.getTotalsFromActivities({ exchangeRates, orders });
} = this.getTotalsFromActivities({ activities, exchangeRates });
const dateOfFirstActivity = parseDate(orders[0].date);
const dateOfFirstActivity = parseDate(activities[0].date);
const endDateString = format(end, DATE_FORMAT);
const startDateString = format(start, DATE_FORMAT);
@ -211,9 +211,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
marketSymbolMap[startDateString]?.[assetProfileIdentifier];
const unitPriceAtEndDate = this.getUnitPriceAtEndDate({
activities,
dataSource,
isCash,
orders,
marketPriceAtEndDate:
marketSymbolMap[endDateString]?.[assetProfileIdentifier]
});
@ -225,7 +225,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
// 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 = orders.some(({ type }) => {
const hasActivitiesWithQuantity = activities.some(({ type }) => {
return ['BUY', 'SELL'].includes(type);
});
@ -239,11 +239,11 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
};
}
orders = this.getOrdersWithMarketPrices({
activities = this.getActivitiesWithMarketPrices({
activities,
chartDates,
endDateString,
marketSymbolMap,
orders,
startDateString,
unitPriceAtEndDate,
unitPriceAtStartDate,
@ -254,11 +254,11 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}
});
const indexOfStartOrder = orders.findIndex(({ itemType }) => {
const indexOfStartActivity = activities.findIndex(({ itemType }) => {
return itemType === 'start';
});
const indexOfEndOrder = orders.findIndex(({ itemType }) => {
const indexOfEndActivity = activities.findIndex(({ itemType }) => {
return itemType === 'end';
});
@ -266,56 +266,59 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let sumOfTimeWeightedInvestments = new Big(0);
let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0);
for (let i = 0; i < orders.length; i += 1) {
const order = orders[i];
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,
order.date,
order.type,
order.itemType ? `(${order.itemType})` : ''
activity.date,
activity.type,
activity.itemType ? `(${activity.itemType})` : ''
);
}
const exchangeRateAtOrderDate = exchangeRates[order.date];
const exchangeRateAtActivityDate = exchangeRates[activity.date];
if (order.itemType === 'start') {
// Take the unit price of the order as the market price if there are no
// orders of this symbol before the start date
order.unitPrice =
indexOfStartOrder === 0
? orders[i + 1]?.unitPrice
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 (order.fee) {
order.feeInBaseCurrency = order.fee.mul(currentExchangeRate ?? 1);
order.feeInBaseCurrencyWithCurrencyEffect = order.fee.mul(
exchangeRateAtOrderDate ?? 1
if (activity.fee) {
activity.feeInBaseCurrency = activity.fee.mul(currentExchangeRate ?? 1);
activity.feeInBaseCurrencyWithCurrencyEffect = activity.fee.mul(
exchangeRateAtActivityDate ?? 1
);
}
const unitPrice = ['BUY', 'SELL'].includes(order.type)
? order.unitPrice
: order.unitPriceFromMarketData;
const unitPrice = ['BUY', 'SELL'].includes(activity.type)
? activity.unitPrice
: activity.unitPriceFromMarketData;
if (unitPrice) {
order.unitPriceInBaseCurrency = unitPrice.mul(currentExchangeRate ?? 1);
activity.unitPriceInBaseCurrency = unitPrice.mul(
currentExchangeRate ?? 1
);
order.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul(
exchangeRateAtOrderDate ?? 1
activity.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul(
exchangeRateAtActivityDate ?? 1
);
}
const marketPriceInBaseCurrency =
order.unitPriceFromMarketData?.mul(currentExchangeRate ?? 1) ??
activity.unitPriceFromMarketData?.mul(currentExchangeRate ?? 1) ??
new Big(0);
const marketPriceInBaseCurrencyWithCurrencyEffect =
order.unitPriceFromMarketData?.mul(exchangeRateAtOrderDate ?? 1) ??
new Big(0);
activity.unitPriceFromMarketData?.mul(
exchangeRateAtActivityDate ?? 1
) ?? new Big(0);
const valueOfInvestmentBeforeTransaction = totalQuantity.mul(
marketPriceInBaseCurrency
@ -324,7 +327,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
const valueOfInvestmentBeforeTransactionWithCurrencyEffect =
totalQuantity.mul(marketPriceInBaseCurrencyWithCurrencyEffect);
if (!investmentAtStartDate && i >= indexOfStartOrder) {
if (!investmentAtStartDate && i >= indexOfStartActivity) {
investmentAtStartDate = totalInvestment ?? new Big(0);
investmentAtStartDateWithCurrencyEffect =
@ -339,17 +342,17 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let transactionInvestment = new Big(0);
let transactionInvestmentWithCurrencyEffect = new Big(0);
if (order.type === 'BUY') {
transactionInvestment = order.quantity
.mul(order.unitPriceInBaseCurrency)
.mul(getFactor(order.type));
if (activity.type === 'BUY') {
transactionInvestment = activity.quantity
.mul(activity.unitPriceInBaseCurrency)
.mul(getFactor(activity.type));
transactionInvestmentWithCurrencyEffect = order.quantity
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect)
.mul(getFactor(order.type));
transactionInvestmentWithCurrencyEffect = activity.quantity
.mul(activity.unitPriceInBaseCurrencyWithCurrencyEffect)
.mul(getFactor(activity.type));
totalQuantityFromBuyTransactions =
totalQuantityFromBuyTransactions.plus(order.quantity);
totalQuantityFromBuyTransactions.plus(activity.quantity);
totalInvestmentFromBuyTransactions =
totalInvestmentFromBuyTransactions.plus(transactionInvestment);
@ -358,9 +361,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus(
transactionInvestmentWithCurrencyEffect
);
} else if (order.type === 'SELL') {
} else if (activity.type === 'SELL') {
if (totalQuantity.gt(0)) {
const remainingQuantity = totalQuantity.minus(order.quantity);
const remainingQuantity = totalQuantity.minus(activity.quantity);
transactionInvestment = totalInvestment
.mul(remainingQuantity)
@ -376,7 +379,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log('order.quantity', order.quantity.toNumber());
console.log('activity.quantity', activity.quantity.toNumber());
console.log('transactionInvestment', transactionInvestment.toNumber());
console.log(
@ -397,9 +400,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
transactionInvestmentWithCurrencyEffect
);
if (i >= indexOfStartOrder && !initialValue) {
if (i >= indexOfStartActivity && !initialValue) {
if (
i === indexOfStartOrder &&
i === indexOfStartActivity &&
!valueOfInvestmentBeforeTransaction.eq(0)
) {
initialValue = valueOfInvestmentBeforeTransaction;
@ -408,14 +411,14 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}
}
fees = fees.plus(order.feeInBaseCurrency ?? 0);
fees = fees.plus(activity.feeInBaseCurrency ?? 0);
feesWithCurrencyEffect = feesWithCurrencyEffect.plus(
order.feeInBaseCurrencyWithCurrencyEffect ?? 0
activity.feeInBaseCurrencyWithCurrencyEffect ?? 0
);
totalQuantity = totalQuantity.plus(
order.quantity.mul(getFactor(order.type))
activity.quantity.mul(getFactor(activity.type))
);
const valueOfInvestment = totalQuantity.mul(marketPriceInBaseCurrency);
@ -425,17 +428,17 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
);
const grossPerformanceFromSell =
order.type === 'SELL'
? order.unitPriceInBaseCurrency
activity.type === 'SELL'
? activity.unitPriceInBaseCurrency
.minus(lastAveragePrice)
.mul(order.quantity)
.mul(activity.quantity)
: new Big(0);
const grossPerformanceFromSellWithCurrencyEffect =
order.type === 'SELL'
? order.unitPriceInBaseCurrencyWithCurrencyEffect
activity.type === 'SELL'
? activity.unitPriceInBaseCurrencyWithCurrencyEffect
.minus(lastAveragePriceWithCurrencyEffect)
.mul(order.quantity)
.mul(activity.quantity)
: new Big(0);
grossPerformanceFromSells = grossPerformanceFromSells.plus(
@ -493,7 +496,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
grossPerformanceWithCurrencyEffect =
newGrossPerformanceWithCurrencyEffect;
if (order.itemType === 'start') {
if (activity.itemType === 'start') {
feesAtStartDate = fees;
feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect;
grossPerformanceAtStartDate = grossPerformance;
@ -502,36 +505,36 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
grossPerformanceWithCurrencyEffect;
}
if (i > indexOfStartOrder) {
if (i > indexOfStartActivity) {
// Only consider periods with an investment for the calculation of
// the time weighted investment
if (
valueOfInvestmentBeforeTransaction.gt(0) &&
['BUY', 'SELL'].includes(order.type)
['BUY', 'SELL'].includes(activity.type)
) {
// Calculate the number of days since the previous order
const orderDate = new Date(order.date);
const previousOrderDate = new Date(orders[i - 1].date);
// Calculate the number of days since the previous activity
const activityDate = new Date(activity.date);
const previousActivityDate = new Date(activities[i - 1].date);
let daysSinceLastOrder = differenceInDays(
orderDate,
previousOrderDate
let daysSinceLastActivity = differenceInDays(
activityDate,
previousActivityDate
);
if (daysSinceLastOrder <= 0) {
if (daysSinceLastActivity <= 0) {
// The time between two activities on the same day is unknown
// -> Set it to the smallest floating point number greater than 0
daysSinceLastOrder = Number.EPSILON;
daysSinceLastActivity = Number.EPSILON;
}
// Sum up the total investment days since the start date to calculate
// the time weighted investment
totalInvestmentDays += daysSinceLastOrder;
totalInvestmentDays += daysSinceLastActivity;
sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add(
valueAtStartDate
.minus(investmentAtStartDate)
.plus(totalInvestmentBeforeTransaction)
.mul(daysSinceLastOrder)
.mul(daysSinceLastActivity)
);
sumOfTimeWeightedInvestmentsWithCurrencyEffect =
@ -539,45 +542,45 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
valueAtStartDateWithCurrencyEffect
.minus(investmentAtStartDateWithCurrencyEffect)
.plus(totalInvestmentBeforeTransactionWithCurrencyEffect)
.mul(daysSinceLastOrder)
.mul(daysSinceLastActivity)
);
}
currentValues[order.date] = valueOfInvestment;
currentValues[activity.date] = valueOfInvestment;
currentValuesWithCurrencyEffect[order.date] =
currentValuesWithCurrencyEffect[activity.date] =
valueOfInvestmentWithCurrencyEffect;
netPerformanceValues[order.date] = grossPerformance
netPerformanceValues[activity.date] = grossPerformance
.minus(grossPerformanceAtStartDate)
.minus(fees.minus(feesAtStartDate));
netPerformanceValuesWithCurrencyEffect[order.date] =
netPerformanceValuesWithCurrencyEffect[activity.date] =
grossPerformanceWithCurrencyEffect
.minus(grossPerformanceAtStartDateWithCurrencyEffect)
.minus(
feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect)
);
investmentValuesAccumulated[order.date] = totalInvestment;
investmentValuesAccumulated[activity.date] = totalInvestment;
investmentValuesAccumulatedWithCurrencyEffect[order.date] =
investmentValuesAccumulatedWithCurrencyEffect[activity.date] =
totalInvestmentWithCurrencyEffect;
investmentValuesWithCurrencyEffect[order.date] = (
investmentValuesWithCurrencyEffect[order.date] ?? new Big(0)
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[order.date] =
timeWeightedInvestmentValues[activity.date] =
totalInvestmentDays > Number.EPSILON
? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
: totalInvestment.gt(0)
? totalInvestment
: new Big(0);
timeWeightedInvestmentValuesWithCurrencyEffect[order.date] =
timeWeightedInvestmentValuesWithCurrencyEffect[activity.date] =
totalInvestmentDays > Number.EPSILON
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div(
totalInvestmentDays
@ -608,7 +611,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
);
}
if (i === indexOfEndOrder) {
if (i === indexOfEndActivity) {
break;
}
}
@ -768,7 +771,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
console.log(
`
${symbol}
Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed(
Unit price: ${activities[indexOfStartActivity].unitPrice.toFixed(
2
)} -> ${unitPriceAtEndDate.toFixed(2)}
Total investment: ${totalInvestment.toFixed(2)}

4
apps/api/src/app/portfolio/interfaces/portfolio-order-item.interface.ts → apps/api/src/app/portfolio/interfaces/portfolio-calculator-activity-item.interface.ts

@ -1,8 +1,8 @@
import { Big } from 'big.js';
import { PortfolioOrder } from './portfolio-order.interface';
import { PortfolioCalculatorActivity } from './portfolio-calculator-activity.interface';
export interface PortfolioOrderItem extends PortfolioOrder {
export interface PortfolioCalculatorActivityItem extends PortfolioCalculatorActivity {
feeInBaseCurrencyWithCurrencyEffect?: Big;
itemType?: 'end' | 'start';
unitPriceFromMarketData?: Big;

5
apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts → apps/api/src/app/portfolio/interfaces/portfolio-calculator-activity.interface.ts

@ -1,6 +1,9 @@
import { Activity } from '@ghostfolio/common/interfaces';
export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> {
export interface PortfolioCalculatorActivity extends Pick<
Activity,
'tags' | 'type'
> {
assetProfile: Pick<
Activity['assetProfile'],
'assetSubClass' | 'currency' | 'dataSource' | 'name' | 'symbol' | 'userId'
Loading…
Cancel
Save