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 { 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 { 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 { 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';
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 { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface';
import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { 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 readonly logger = new Logger(PortfolioCalculator.name);
protected accountBalanceItems: HistoricalDataItem[]; protected accountBalanceItems: HistoricalDataItem[];
protected activities: PortfolioOrder[]; protected activities: PortfolioCalculatorActivity[];
protected activitiesByAssetProfileIdentifier: { protected activitiesByAssetProfileIdentifier: {
[assetProfileIdentifier: string]: PortfolioOrder[]; [assetProfileIdentifier: string]: PortfolioCalculatorActivity[];
}; };
private configurationService: ConfigurationService; 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() { public getDataProviderInfos() {
return this.dataProviderInfos; return this.dataProviderInfos;
} }
@ -837,137 +970,6 @@ export abstract class PortfolioCalculator {
return this.snapshot.totalLiabilitiesWithCurrencyEffect; 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 }) { public async getPerformance({ end, start }) {
await this.snapshotPromise; await this.snapshotPromise;
@ -1104,38 +1106,38 @@ export abstract class PortfolioCalculator {
} }
protected getTotalsFromActivities({ protected getTotalsFromActivities({
exchangeRates, activities,
orders exchangeRates
}: { }: {
activities: PortfolioCalculatorActivity[];
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
orders: PortfolioOrder[];
}) { }) {
let totalDividend = new Big(0); let totalDividend = new Big(0);
let totalDividendInBaseCurrency = new Big(0); let totalDividendInBaseCurrency = new Big(0);
let totalInterestInBaseCurrency = new Big(0); let totalInterestInBaseCurrency = new Big(0);
let totalLiabilitiesInBaseCurrency = new Big(0); let totalLiabilitiesInBaseCurrency = new Big(0);
for (const order of orders) { for (const activity of activities) {
const exchangeRateAtOrderDate = exchangeRates[order.date]; const exchangeRateAtActivityDate = exchangeRates[activity.date];
if (order.type === 'DIVIDEND') { if (activity.type === 'DIVIDEND') {
const dividend = order.quantity.mul(order.unitPrice); const dividend = activity.quantity.mul(activity.unitPrice);
totalDividend = totalDividend.plus(dividend); totalDividend = totalDividend.plus(dividend);
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus( totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
dividend.mul(exchangeRateAtOrderDate ?? 1) dividend.mul(exchangeRateAtActivityDate ?? 1)
); );
} else if (order.type === 'INTEREST') { } else if (activity.type === 'INTEREST') {
const interest = order.quantity.mul(order.unitPrice); const interest = activity.quantity.mul(activity.unitPrice);
totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus( totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus(
interest.mul(exchangeRateAtOrderDate ?? 1) interest.mul(exchangeRateAtActivityDate ?? 1)
); );
} else if (order.type === 'LIABILITY') { } else if (activity.type === 'LIABILITY') {
const liabilities = order.quantity.mul(order.unitPrice); const liabilities = activity.quantity.mul(activity.unitPrice);
totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus( totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus(
liabilities.mul(exchangeRateAtOrderDate ?? 1) liabilities.mul(exchangeRateAtActivityDate ?? 1)
); );
} }
} }
@ -1153,17 +1155,17 @@ export abstract class PortfolioCalculator {
} }
protected getUnitPriceAtEndDate({ protected getUnitPriceAtEndDate({
activities,
dataSource, dataSource,
isCash, isCash,
marketPriceAtEndDate, marketPriceAtEndDate
orders
}: { }: {
activities: PortfolioCalculatorActivity[];
dataSource: DataSource; dataSource: DataSource;
isCash: boolean; isCash: boolean;
marketPriceAtEndDate: Big; marketPriceAtEndDate: Big;
orders: PortfolioOrder[];
}): Big { }): Big {
const latestActivity = orders.at(-1); const latestActivity = activities.at(-1);
if ( if (
dataSource === 'MANUAL' && 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 { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
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 { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { import {
@ -183,12 +183,12 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
symbol symbol
}); });
let orders: PortfolioOrderItem[] = let activities: PortfolioCalculatorActivityItem[] =
this.activitiesByAssetProfileIdentifier[assetProfileIdentifier] ?? []; 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(); return this.getEmptyHoldingPerformance();
} }
@ -200,9 +200,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
totalDividendInBaseCurrency, totalDividendInBaseCurrency,
totalInterestInBaseCurrency, totalInterestInBaseCurrency,
totalLiabilitiesInBaseCurrency 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 endDateString = format(end, DATE_FORMAT);
const startDateString = format(start, DATE_FORMAT); const startDateString = format(start, DATE_FORMAT);
@ -211,9 +211,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
marketSymbolMap[startDateString]?.[assetProfileIdentifier]; marketSymbolMap[startDateString]?.[assetProfileIdentifier];
const unitPriceAtEndDate = this.getUnitPriceAtEndDate({ const unitPriceAtEndDate = this.getUnitPriceAtEndDate({
activities,
dataSource, dataSource,
isCash, isCash,
orders,
marketPriceAtEndDate: marketPriceAtEndDate:
marketSymbolMap[endDateString]?.[assetProfileIdentifier] 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 // A missing market price can only affect the quantity which is held. The
// dividends, the interest and the liabilities do not hold any quantity // dividends, the interest and the liabilities do not hold any quantity
// and are therefore not in error. // and are therefore not in error.
const hasActivitiesWithQuantity = orders.some(({ type }) => { const hasActivitiesWithQuantity = activities.some(({ type }) => {
return ['BUY', 'SELL'].includes(type); return ['BUY', 'SELL'].includes(type);
}); });
@ -239,11 +239,11 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}; };
} }
orders = this.getOrdersWithMarketPrices({ activities = this.getActivitiesWithMarketPrices({
activities,
chartDates, chartDates,
endDateString, endDateString,
marketSymbolMap, marketSymbolMap,
orders,
startDateString, startDateString,
unitPriceAtEndDate, unitPriceAtEndDate,
unitPriceAtStartDate, unitPriceAtStartDate,
@ -254,11 +254,11 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
} }
}); });
const indexOfStartOrder = orders.findIndex(({ itemType }) => { const indexOfStartActivity = activities.findIndex(({ itemType }) => {
return itemType === 'start'; return itemType === 'start';
}); });
const indexOfEndOrder = orders.findIndex(({ itemType }) => { const indexOfEndActivity = activities.findIndex(({ itemType }) => {
return itemType === 'end'; return itemType === 'end';
}); });
@ -266,56 +266,59 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let sumOfTimeWeightedInvestments = new Big(0); let sumOfTimeWeightedInvestments = new Big(0);
let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0); let sumOfTimeWeightedInvestmentsWithCurrencyEffect = new Big(0);
for (let i = 0; i < orders.length; i += 1) { for (let i = 0; i < activities.length; i += 1) {
const order = orders[i]; const activity = activities[i];
if (PortfolioCalculator.ENABLE_LOGGING) { if (PortfolioCalculator.ENABLE_LOGGING) {
console.log(); console.log();
console.log(); console.log();
console.log( console.log(
i + 1, i + 1,
order.date, activity.date,
order.type, activity.type,
order.itemType ? `(${order.itemType})` : '' activity.itemType ? `(${activity.itemType})` : ''
); );
} }
const exchangeRateAtOrderDate = exchangeRates[order.date]; const exchangeRateAtActivityDate = exchangeRates[activity.date];
if (order.itemType === 'start') { if (activity.itemType === 'start') {
// Take the unit price of the order as the market price if there are no // Take the unit price of the activity as the market price if there are no
// orders of this symbol before the start date // activities of this symbol before the start date
order.unitPrice = activity.unitPrice =
indexOfStartOrder === 0 indexOfStartActivity === 0
? orders[i + 1]?.unitPrice ? activities[i + 1]?.unitPrice
: unitPriceAtStartDate; : unitPriceAtStartDate;
} }
if (order.fee) { if (activity.fee) {
order.feeInBaseCurrency = order.fee.mul(currentExchangeRate ?? 1); activity.feeInBaseCurrency = activity.fee.mul(currentExchangeRate ?? 1);
order.feeInBaseCurrencyWithCurrencyEffect = order.fee.mul( activity.feeInBaseCurrencyWithCurrencyEffect = activity.fee.mul(
exchangeRateAtOrderDate ?? 1 exchangeRateAtActivityDate ?? 1
); );
} }
const unitPrice = ['BUY', 'SELL'].includes(order.type) const unitPrice = ['BUY', 'SELL'].includes(activity.type)
? order.unitPrice ? activity.unitPrice
: order.unitPriceFromMarketData; : activity.unitPriceFromMarketData;
if (unitPrice) { if (unitPrice) {
order.unitPriceInBaseCurrency = unitPrice.mul(currentExchangeRate ?? 1); activity.unitPriceInBaseCurrency = unitPrice.mul(
currentExchangeRate ?? 1
);
order.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul( activity.unitPriceInBaseCurrencyWithCurrencyEffect = unitPrice.mul(
exchangeRateAtOrderDate ?? 1 exchangeRateAtActivityDate ?? 1
); );
} }
const marketPriceInBaseCurrency = const marketPriceInBaseCurrency =
order.unitPriceFromMarketData?.mul(currentExchangeRate ?? 1) ?? activity.unitPriceFromMarketData?.mul(currentExchangeRate ?? 1) ??
new Big(0); new Big(0);
const marketPriceInBaseCurrencyWithCurrencyEffect = const marketPriceInBaseCurrencyWithCurrencyEffect =
order.unitPriceFromMarketData?.mul(exchangeRateAtOrderDate ?? 1) ?? activity.unitPriceFromMarketData?.mul(
new Big(0); exchangeRateAtActivityDate ?? 1
) ?? new Big(0);
const valueOfInvestmentBeforeTransaction = totalQuantity.mul( const valueOfInvestmentBeforeTransaction = totalQuantity.mul(
marketPriceInBaseCurrency marketPriceInBaseCurrency
@ -324,7 +327,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
const valueOfInvestmentBeforeTransactionWithCurrencyEffect = const valueOfInvestmentBeforeTransactionWithCurrencyEffect =
totalQuantity.mul(marketPriceInBaseCurrencyWithCurrencyEffect); totalQuantity.mul(marketPriceInBaseCurrencyWithCurrencyEffect);
if (!investmentAtStartDate && i >= indexOfStartOrder) { if (!investmentAtStartDate && i >= indexOfStartActivity) {
investmentAtStartDate = totalInvestment ?? new Big(0); investmentAtStartDate = totalInvestment ?? new Big(0);
investmentAtStartDateWithCurrencyEffect = investmentAtStartDateWithCurrencyEffect =
@ -339,17 +342,17 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let transactionInvestment = new Big(0); let transactionInvestment = new Big(0);
let transactionInvestmentWithCurrencyEffect = new Big(0); let transactionInvestmentWithCurrencyEffect = new Big(0);
if (order.type === 'BUY') { if (activity.type === 'BUY') {
transactionInvestment = order.quantity transactionInvestment = activity.quantity
.mul(order.unitPriceInBaseCurrency) .mul(activity.unitPriceInBaseCurrency)
.mul(getFactor(order.type)); .mul(getFactor(activity.type));
transactionInvestmentWithCurrencyEffect = order.quantity transactionInvestmentWithCurrencyEffect = activity.quantity
.mul(order.unitPriceInBaseCurrencyWithCurrencyEffect) .mul(activity.unitPriceInBaseCurrencyWithCurrencyEffect)
.mul(getFactor(order.type)); .mul(getFactor(activity.type));
totalQuantityFromBuyTransactions = totalQuantityFromBuyTransactions =
totalQuantityFromBuyTransactions.plus(order.quantity); totalQuantityFromBuyTransactions.plus(activity.quantity);
totalInvestmentFromBuyTransactions = totalInvestmentFromBuyTransactions =
totalInvestmentFromBuyTransactions.plus(transactionInvestment); totalInvestmentFromBuyTransactions.plus(transactionInvestment);
@ -358,9 +361,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus( totalInvestmentFromBuyTransactionsWithCurrencyEffect.plus(
transactionInvestmentWithCurrencyEffect transactionInvestmentWithCurrencyEffect
); );
} else if (order.type === 'SELL') { } else if (activity.type === 'SELL') {
if (totalQuantity.gt(0)) { if (totalQuantity.gt(0)) {
const remainingQuantity = totalQuantity.minus(order.quantity); const remainingQuantity = totalQuantity.minus(activity.quantity);
transactionInvestment = totalInvestment transactionInvestment = totalInvestment
.mul(remainingQuantity) .mul(remainingQuantity)
@ -376,7 +379,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
} }
if (PortfolioCalculator.ENABLE_LOGGING) { 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('transactionInvestment', transactionInvestment.toNumber());
console.log( console.log(
@ -397,9 +400,9 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
transactionInvestmentWithCurrencyEffect transactionInvestmentWithCurrencyEffect
); );
if (i >= indexOfStartOrder && !initialValue) { if (i >= indexOfStartActivity && !initialValue) {
if ( if (
i === indexOfStartOrder && i === indexOfStartActivity &&
!valueOfInvestmentBeforeTransaction.eq(0) !valueOfInvestmentBeforeTransaction.eq(0)
) { ) {
initialValue = valueOfInvestmentBeforeTransaction; 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( feesWithCurrencyEffect = feesWithCurrencyEffect.plus(
order.feeInBaseCurrencyWithCurrencyEffect ?? 0 activity.feeInBaseCurrencyWithCurrencyEffect ?? 0
); );
totalQuantity = totalQuantity.plus( totalQuantity = totalQuantity.plus(
order.quantity.mul(getFactor(order.type)) activity.quantity.mul(getFactor(activity.type))
); );
const valueOfInvestment = totalQuantity.mul(marketPriceInBaseCurrency); const valueOfInvestment = totalQuantity.mul(marketPriceInBaseCurrency);
@ -425,17 +428,17 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
); );
const grossPerformanceFromSell = const grossPerformanceFromSell =
order.type === 'SELL' activity.type === 'SELL'
? order.unitPriceInBaseCurrency ? activity.unitPriceInBaseCurrency
.minus(lastAveragePrice) .minus(lastAveragePrice)
.mul(order.quantity) .mul(activity.quantity)
: new Big(0); : new Big(0);
const grossPerformanceFromSellWithCurrencyEffect = const grossPerformanceFromSellWithCurrencyEffect =
order.type === 'SELL' activity.type === 'SELL'
? order.unitPriceInBaseCurrencyWithCurrencyEffect ? activity.unitPriceInBaseCurrencyWithCurrencyEffect
.minus(lastAveragePriceWithCurrencyEffect) .minus(lastAveragePriceWithCurrencyEffect)
.mul(order.quantity) .mul(activity.quantity)
: new Big(0); : new Big(0);
grossPerformanceFromSells = grossPerformanceFromSells.plus( grossPerformanceFromSells = grossPerformanceFromSells.plus(
@ -493,7 +496,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
grossPerformanceWithCurrencyEffect = grossPerformanceWithCurrencyEffect =
newGrossPerformanceWithCurrencyEffect; newGrossPerformanceWithCurrencyEffect;
if (order.itemType === 'start') { if (activity.itemType === 'start') {
feesAtStartDate = fees; feesAtStartDate = fees;
feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect; feesAtStartDateWithCurrencyEffect = feesWithCurrencyEffect;
grossPerformanceAtStartDate = grossPerformance; grossPerformanceAtStartDate = grossPerformance;
@ -502,36 +505,36 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
grossPerformanceWithCurrencyEffect; grossPerformanceWithCurrencyEffect;
} }
if (i > indexOfStartOrder) { if (i > indexOfStartActivity) {
// Only consider periods with an investment for the calculation of // Only consider periods with an investment for the calculation of
// the time weighted investment // the time weighted investment
if ( if (
valueOfInvestmentBeforeTransaction.gt(0) && valueOfInvestmentBeforeTransaction.gt(0) &&
['BUY', 'SELL'].includes(order.type) ['BUY', 'SELL'].includes(activity.type)
) { ) {
// Calculate the number of days since the previous order // Calculate the number of days since the previous activity
const orderDate = new Date(order.date); const activityDate = new Date(activity.date);
const previousOrderDate = new Date(orders[i - 1].date); const previousActivityDate = new Date(activities[i - 1].date);
let daysSinceLastOrder = differenceInDays( let daysSinceLastActivity = differenceInDays(
orderDate, activityDate,
previousOrderDate previousActivityDate
); );
if (daysSinceLastOrder <= 0) { if (daysSinceLastActivity <= 0) {
// The time between two activities on the same day is unknown // The time between two activities on the same day is unknown
// -> Set it to the smallest floating point number greater than 0 // -> 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 // Sum up the total investment days since the start date to calculate
// the time weighted investment // the time weighted investment
totalInvestmentDays += daysSinceLastOrder; totalInvestmentDays += daysSinceLastActivity;
sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add( sumOfTimeWeightedInvestments = sumOfTimeWeightedInvestments.add(
valueAtStartDate valueAtStartDate
.minus(investmentAtStartDate) .minus(investmentAtStartDate)
.plus(totalInvestmentBeforeTransaction) .plus(totalInvestmentBeforeTransaction)
.mul(daysSinceLastOrder) .mul(daysSinceLastActivity)
); );
sumOfTimeWeightedInvestmentsWithCurrencyEffect = sumOfTimeWeightedInvestmentsWithCurrencyEffect =
@ -539,45 +542,45 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
valueAtStartDateWithCurrencyEffect valueAtStartDateWithCurrencyEffect
.minus(investmentAtStartDateWithCurrencyEffect) .minus(investmentAtStartDateWithCurrencyEffect)
.plus(totalInvestmentBeforeTransactionWithCurrencyEffect) .plus(totalInvestmentBeforeTransactionWithCurrencyEffect)
.mul(daysSinceLastOrder) .mul(daysSinceLastActivity)
); );
} }
currentValues[order.date] = valueOfInvestment; currentValues[activity.date] = valueOfInvestment;
currentValuesWithCurrencyEffect[order.date] = currentValuesWithCurrencyEffect[activity.date] =
valueOfInvestmentWithCurrencyEffect; valueOfInvestmentWithCurrencyEffect;
netPerformanceValues[order.date] = grossPerformance netPerformanceValues[activity.date] = grossPerformance
.minus(grossPerformanceAtStartDate) .minus(grossPerformanceAtStartDate)
.minus(fees.minus(feesAtStartDate)); .minus(fees.minus(feesAtStartDate));
netPerformanceValuesWithCurrencyEffect[order.date] = netPerformanceValuesWithCurrencyEffect[activity.date] =
grossPerformanceWithCurrencyEffect grossPerformanceWithCurrencyEffect
.minus(grossPerformanceAtStartDateWithCurrencyEffect) .minus(grossPerformanceAtStartDateWithCurrencyEffect)
.minus( .minus(
feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect) feesWithCurrencyEffect.minus(feesAtStartDateWithCurrencyEffect)
); );
investmentValuesAccumulated[order.date] = totalInvestment; investmentValuesAccumulated[activity.date] = totalInvestment;
investmentValuesAccumulatedWithCurrencyEffect[order.date] = investmentValuesAccumulatedWithCurrencyEffect[activity.date] =
totalInvestmentWithCurrencyEffect; totalInvestmentWithCurrencyEffect;
investmentValuesWithCurrencyEffect[order.date] = ( investmentValuesWithCurrencyEffect[activity.date] = (
investmentValuesWithCurrencyEffect[order.date] ?? new Big(0) investmentValuesWithCurrencyEffect[activity.date] ?? new Big(0)
).add(transactionInvestmentWithCurrencyEffect); ).add(transactionInvestmentWithCurrencyEffect);
// If duration is effectively zero (first day), use the actual investment as the base. // If duration is effectively zero (first day), use the actual investment as the base.
// Otherwise, use the calculated time-weighted average. // Otherwise, use the calculated time-weighted average.
timeWeightedInvestmentValues[order.date] = timeWeightedInvestmentValues[activity.date] =
totalInvestmentDays > Number.EPSILON totalInvestmentDays > Number.EPSILON
? sumOfTimeWeightedInvestments.div(totalInvestmentDays) ? sumOfTimeWeightedInvestments.div(totalInvestmentDays)
: totalInvestment.gt(0) : totalInvestment.gt(0)
? totalInvestment ? totalInvestment
: new Big(0); : new Big(0);
timeWeightedInvestmentValuesWithCurrencyEffect[order.date] = timeWeightedInvestmentValuesWithCurrencyEffect[activity.date] =
totalInvestmentDays > Number.EPSILON totalInvestmentDays > Number.EPSILON
? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div( ? sumOfTimeWeightedInvestmentsWithCurrencyEffect.div(
totalInvestmentDays totalInvestmentDays
@ -608,7 +611,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
); );
} }
if (i === indexOfEndOrder) { if (i === indexOfEndActivity) {
break; break;
} }
} }
@ -768,7 +771,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
console.log( console.log(
` `
${symbol} ${symbol}
Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed( Unit price: ${activities[indexOfStartActivity].unitPrice.toFixed(
2 2
)} -> ${unitPriceAtEndDate.toFixed(2)} )} -> ${unitPriceAtEndDate.toFixed(2)}
Total investment: ${totalInvestment.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 { 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; feeInBaseCurrencyWithCurrencyEffect?: Big;
itemType?: 'end' | 'start'; itemType?: 'end' | 'start';
unitPriceFromMarketData?: Big; 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'; import { Activity } from '@ghostfolio/common/interfaces';
export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> { export interface PortfolioCalculatorActivity extends Pick<
Activity,
'tags' | 'type'
> {
assetProfile: Pick< assetProfile: Pick<
Activity['assetProfile'], Activity['assetProfile'],
'assetSubClass' | 'currency' | 'dataSource' | 'name' | 'symbol' | 'userId' 'assetSubClass' | 'currency' | 'dataSource' | 'name' | 'symbol' | 'userId'
Loading…
Cancel
Save