Browse Source

Task/move method independent helpers of portfolio calculator to base class (#7914)

Move method-independent helpers of portfolio calculator from ROAI calculator into base class
pull/7916/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
3eec62bca4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts
  2. 246
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 250
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts
  4. 2
      apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts
  5. 2
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

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

@ -11,7 +11,7 @@ export class MwrPortfolioCalculator extends PortfolioCalculator {
} }
protected getHoldingPerformance({}: { protected getHoldingPerformance({}: {
chartDateMap: { [date: string]: boolean }; chartDates: string[];
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {

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

@ -2,6 +2,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s
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 { 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 { 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';
@ -43,10 +44,11 @@ import { GroupBy } 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 { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { AssetSubClass } from '@prisma/client'; import { AssetSubClass, DataSource } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { plainToClass } from 'class-transformer'; import { plainToClass } from 'class-transformer';
import { import {
addMilliseconds,
differenceInDays, differenceInDays,
eachDayOfInterval, eachDayOfInterval,
eachYearOfInterval, eachYearOfInterval,
@ -425,7 +427,7 @@ export abstract class PortfolioCalculator {
totalInvestmentWithCurrencyEffect, totalInvestmentWithCurrencyEffect,
totalLiabilitiesInBaseCurrency totalLiabilitiesInBaseCurrency
} = this.getHoldingPerformance({ } = this.getHoldingPerformance({
chartDateMap, chartDates,
marketSymbolMap, marketSymbolMap,
dataSource: item.dataSource, dataSource: item.dataSource,
end: this.endDate, end: this.endDate,
@ -728,6 +730,37 @@ export abstract class PortfolioCalculator {
); );
} }
protected getEmptyHoldingPerformance(): HoldingPerformance {
return {
currentValues: {},
currentValuesWithCurrencyEffect: {},
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
grossPerformanceWithCurrencyEffect: new Big(0),
hasErrors: false,
investmentValuesAccumulated: {},
investmentValuesAccumulatedWithCurrencyEffect: {},
investmentValuesWithCurrencyEffect: {},
netPerformance: new Big(0),
netPerformancePercentage: new Big(0),
netPerformancePercentageWithCurrencyEffectMap: {},
netPerformanceValues: {},
netPerformanceValuesWithCurrencyEffect: {},
netPerformanceWithCurrencyEffectMap: {},
timeWeightedInvestment: new Big(0),
timeWeightedInvestmentValues: {},
timeWeightedInvestmentValuesWithCurrencyEffect: {},
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
totalDividend: new Big(0),
totalDividendInBaseCurrency: new Big(0),
totalInterestInBaseCurrency: new Big(0),
totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0),
totalLiabilitiesInBaseCurrency: new Big(0)
};
}
public async getFeesInBaseCurrency() { public async getFeesInBaseCurrency() {
await this.snapshotPromise; await this.snapshotPromise;
@ -735,7 +768,7 @@ export abstract class PortfolioCalculator {
} }
protected abstract getHoldingPerformance({ protected abstract getHoldingPerformance({
chartDateMap, chartDates,
dataSource, dataSource,
end, end,
exchangeRates, exchangeRates,
@ -743,7 +776,7 @@ export abstract class PortfolioCalculator {
start, start,
symbol symbol
}: { }: {
chartDateMap: { [date: string]: boolean }; chartDates: string[];
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {
@ -804,6 +837,137 @@ 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;
@ -939,10 +1103,84 @@ export abstract class PortfolioCalculator {
return min(dates); return min(dates);
} }
protected getTotalsFromActivities({
exchangeRates,
orders
}: {
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];
if (order.type === 'DIVIDEND') {
const dividend = order.quantity.mul(order.unitPrice);
totalDividend = totalDividend.plus(dividend);
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
dividend.mul(exchangeRateAtOrderDate ?? 1)
);
} else if (order.type === 'INTEREST') {
const interest = order.quantity.mul(order.unitPrice);
totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus(
interest.mul(exchangeRateAtOrderDate ?? 1)
);
} else if (order.type === 'LIABILITY') {
const liabilities = order.quantity.mul(order.unitPrice);
totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus(
liabilities.mul(exchangeRateAtOrderDate ?? 1)
);
}
}
return {
totalDividend,
totalDividendInBaseCurrency,
totalInterestInBaseCurrency,
totalLiabilitiesInBaseCurrency
};
}
public getTransactionPoints() { public getTransactionPoints() {
return this.transactionPoints; return this.transactionPoints;
} }
protected getUnitPriceAtEndDate({
dataSource,
isCash,
marketPriceAtEndDate,
orders
}: {
dataSource: DataSource;
isCash: boolean;
marketPriceAtEndDate: Big;
orders: PortfolioOrder[];
}): Big {
const latestActivity = orders.at(-1);
if (
dataSource === 'MANUAL' &&
['BUY', 'SELL'].includes(latestActivity?.type) &&
latestActivity?.unitPrice &&
!marketPriceAtEndDate
) {
// For BUY / SELL activities with a MANUAL data source where no historical market price is available,
// the calculation should fall back to using the activity’s unit price.
return latestActivity.unitPrice;
} else if (isCash) {
return new Big(1);
}
return marketPriceAtEndDate;
}
private getChartDateMap({ private getChartDateMap({
endDate, endDate,
startDate, startDate,

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

@ -16,18 +16,14 @@ import { PerformanceCalculationType } from '@ghostfolio/common/types/performance
import { Big } from 'big.js'; import { Big } from 'big.js';
import { import {
addMilliseconds,
differenceInDays, differenceInDays,
eachYearOfInterval, eachYearOfInterval,
format, format,
isBefore, isBefore,
isThisYear isThisYear
} from 'date-fns'; } from 'date-fns';
import { sortBy } from 'lodash';
export class RoaiPortfolioCalculator extends PortfolioCalculator { export class RoaiPortfolioCalculator extends PortfolioCalculator {
private chartDates: string[];
protected calculateOverallPerformance( protected calculateOverallPerformance(
positions: PortfolioCalculatorHolding[] positions: PortfolioCalculatorHolding[]
): PortfolioSnapshot { ): PortfolioSnapshot {
@ -126,7 +122,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
} }
protected getHoldingPerformance({ protected getHoldingPerformance({
chartDateMap, chartDates,
dataSource, dataSource,
end, end,
exchangeRates, exchangeRates,
@ -134,7 +130,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
start, start,
symbol symbol
}: { }: {
chartDateMap: { [date: string]: boolean }; chartDates: string[];
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {
@ -173,14 +169,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
[date: string]: Big; [date: string]: Big;
} = {}; } = {};
let totalDividend = new Big(0);
let totalDividendInBaseCurrency = new Big(0);
let totalInterestInBaseCurrency = new Big(0);
let totalInvestment = new Big(0); let totalInvestment = new Big(0);
let totalInvestmentFromBuyTransactions = new Big(0); let totalInvestmentFromBuyTransactions = new Big(0);
let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0); let totalInvestmentFromBuyTransactionsWithCurrencyEffect = new Big(0);
let totalInvestmentWithCurrencyEffect = new Big(0); let totalInvestmentWithCurrencyEffect = new Big(0);
let totalLiabilitiesInBaseCurrency = new Big(0);
let totalQuantity = new Big(0); let totalQuantity = new Big(0);
let totalQuantityFromBuyTransactions = new Big(0); let totalQuantityFromBuyTransactions = new Big(0);
let valueAtStartDate: Big; let valueAtStartDate: Big;
@ -191,77 +183,26 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
symbol symbol
}); });
// Copy the items as they are enriched below. A shallow copy is sufficient let orders: PortfolioOrderItem[] =
// because only top-level properties are written. this.activitiesByAssetProfileIdentifier[assetProfileIdentifier] ?? [];
let orders: PortfolioOrderItem[] = (
this.activitiesByAssetProfileIdentifier[assetProfileIdentifier] ?? []
).map((activity) => {
return { ...activity };
});
const isCash = orders[0]?.assetProfile?.assetSubClass === 'CASH'; const isCash = orders[0]?.assetProfile?.assetSubClass === 'CASH';
if (orders.length <= 0) { if (orders.length <= 0) {
return { return this.getEmptyHoldingPerformance();
currentValues: {},
currentValuesWithCurrencyEffect: {},
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
grossPerformanceWithCurrencyEffect: new Big(0),
hasErrors: false,
investmentValuesAccumulated: {},
investmentValuesAccumulatedWithCurrencyEffect: {},
investmentValuesWithCurrencyEffect: {},
netPerformance: new Big(0),
netPerformancePercentage: new Big(0),
netPerformancePercentageWithCurrencyEffectMap: {},
netPerformanceValues: {},
netPerformanceValuesWithCurrencyEffect: {},
netPerformanceWithCurrencyEffectMap: {},
timeWeightedInvestment: new Big(0),
timeWeightedInvestmentValues: {},
timeWeightedInvestmentValuesWithCurrencyEffect: {},
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
totalDividend: new Big(0),
totalDividendInBaseCurrency: new Big(0),
totalInterestInBaseCurrency: new Big(0),
totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0),
totalLiabilitiesInBaseCurrency: new Big(0)
};
} }
// The dividends, the interest and the liabilities are derived from the // The dividends, the interest and the liabilities are derived from the
// activities only. Accumulate them upfront so that they survive the bail // activities only. Accumulate them upfront so that they survive the bail
// out for symbols without a market price below. // out for symbols without a market price below.
for (const order of orders) { const {
const exchangeRateAtOrderDate = exchangeRates[order.date]; totalDividend,
totalDividendInBaseCurrency,
if (order.type === 'DIVIDEND') { totalInterestInBaseCurrency,
const dividend = order.quantity.mul(order.unitPrice); totalLiabilitiesInBaseCurrency
} = this.getTotalsFromActivities({ exchangeRates, orders });
totalDividend = totalDividend.plus(dividend);
totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus(
dividend.mul(exchangeRateAtOrderDate ?? 1)
);
} else if (order.type === 'INTEREST') {
const interest = order.quantity.mul(order.unitPrice);
totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus(
interest.mul(exchangeRateAtOrderDate ?? 1)
);
} else if (order.type === 'LIABILITY') {
const liabilities = order.quantity.mul(order.unitPrice);
totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus(
liabilities.mul(exchangeRateAtOrderDate ?? 1)
);
}
}
const dateStringOfFirstActivity = orders[0].date; const dateOfFirstActivity = parseDate(orders[0].date);
const dateOfFirstActivity = parseDate(dateStringOfFirstActivity);
const endDateString = format(end, DATE_FORMAT); const endDateString = format(end, DATE_FORMAT);
const startDateString = format(start, DATE_FORMAT); const startDateString = format(start, DATE_FORMAT);
@ -269,23 +210,13 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
const unitPriceAtStartDate = const unitPriceAtStartDate =
marketSymbolMap[startDateString]?.[assetProfileIdentifier]; marketSymbolMap[startDateString]?.[assetProfileIdentifier];
let unitPriceAtEndDate = const unitPriceAtEndDate = this.getUnitPriceAtEndDate({
marketSymbolMap[endDateString]?.[assetProfileIdentifier]; dataSource,
isCash,
const latestActivity = orders.at(-1); orders,
marketPriceAtEndDate:
if ( marketSymbolMap[endDateString]?.[assetProfileIdentifier]
dataSource === 'MANUAL' && });
['BUY', 'SELL'].includes(latestActivity?.type) &&
latestActivity?.unitPrice &&
!unitPriceAtEndDate
) {
// For BUY / SELL activities with a MANUAL data source where no historical market price is available,
// the calculation should fall back to using the activity’s unit price.
unitPriceAtEndDate = latestActivity.unitPrice;
} else if (isCash) {
unitPriceAtEndDate = new Big(1);
}
if ( if (
!unitPriceAtEndDate || !unitPriceAtEndDate ||
@ -299,139 +230,28 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
}); });
return { return {
...this.getEmptyHoldingPerformance(),
totalDividend, totalDividend,
totalDividendInBaseCurrency, totalDividendInBaseCurrency,
totalInterestInBaseCurrency, totalInterestInBaseCurrency,
totalLiabilitiesInBaseCurrency, totalLiabilitiesInBaseCurrency,
currentValues: {}, hasErrors: hasActivitiesWithQuantity
currentValuesWithCurrencyEffect: {},
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
grossPerformanceWithCurrencyEffect: new Big(0),
hasErrors: hasActivitiesWithQuantity,
investmentValuesAccumulated: {},
investmentValuesAccumulatedWithCurrencyEffect: {},
investmentValuesWithCurrencyEffect: {},
netPerformance: new Big(0),
netPerformancePercentage: new Big(0),
netPerformancePercentageWithCurrencyEffectMap: {},
netPerformanceWithCurrencyEffectMap: {},
netPerformanceValues: {},
netPerformanceValuesWithCurrencyEffect: {},
timeWeightedInvestment: new Big(0),
timeWeightedInvestmentValues: {},
timeWeightedInvestmentValuesWithCurrencyEffect: {},
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0)
}; };
} }
const assetProfile: PortfolioOrderItem['assetProfile'] = { orders = this.getOrdersWithMarketPrices({
dataSource, chartDates,
symbol, endDateString,
assetSubClass: isCash ? 'CASH' : undefined marketSymbolMap,
}; orders,
startDateString,
// Add a synthetic order at the start and the end date unitPriceAtEndDate,
orders.push({ unitPriceAtStartDate,
assetProfile, assetProfile: {
date: startDateString, dataSource,
fee: new Big(0), symbol,
feeInBaseCurrency: new Big(0), assetSubClass: isCash ? 'CASH' : undefined
itemType: 'start',
quantity: new Big(0),
type: 'BUY',
unitPrice: unitPriceAtStartDate
});
orders.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 orders) {
ordersByDate[order.date] = ordersByDate[order.date] ?? [];
ordersByDate[order.date].push(order);
}
if (!this.chartDates) {
this.chartDates = Object.keys(chartDateMap).sort();
}
for (const dateString of this.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) {
orders.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
orders = sortBy(orders, ({ 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();
}); });
const indexOfStartOrder = orders.findIndex(({ itemType }) => { const indexOfStartOrder = orders.findIndex(({ itemType }) => {
@ -901,8 +721,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
let average = new Big(0); let average = new Big(0);
let dayCount = 0; let dayCount = 0;
for (let i = this.chartDates.length - 1; i >= 0; i -= 1) { for (let i = chartDates.length - 1; i >= 0; i -= 1) {
const date = this.chartDates[i]; const date = chartDates[i];
if (date > rangeEndDateString) { if (date > rangeEndDateString) {
continue; continue;

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

@ -11,7 +11,7 @@ export class RoiPortfolioCalculator extends PortfolioCalculator {
} }
protected getHoldingPerformance({}: { protected getHoldingPerformance({}: {
chartDateMap: { [date: string]: boolean }; chartDates: string[];
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {

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

@ -11,7 +11,7 @@ export class TwrPortfolioCalculator extends PortfolioCalculator {
} }
protected getHoldingPerformance({}: { protected getHoldingPerformance({}: {
chartDateMap: { [date: string]: boolean }; chartDates: string[];
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
marketSymbolMap: { marketSymbolMap: {

Loading…
Cancel
Save