You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

2573 lines
82 KiB

import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import {
DataProviderInfo,
HistoricalDataItem,
InvestmentItem,
ResponseError,
SymbolMetrics,
TimelinePosition
} from '@ghostfolio/common/interfaces';
import { GroupBy } from '@ghostfolio/common/types';
import { Logger } from '@nestjs/common';
import { Type as TypeOfOrder } from '@prisma/client';
import Big from 'big.js';
import {
addDays,
addMilliseconds,
differenceInDays,
endOfDay,
format,
isBefore,
isAfter,
isSameDay,
subDays
} from 'date-fns';
import { cloneDeep, first, isNumber, last, sortBy, uniq } from 'lodash';
import { CurrentRateService } from './current-rate.service';
import { CurrentPositions } from './interfaces/current-positions.interface';
import { GetValueObject } from './interfaces/get-value-object.interface';
import {
PortfolioOrderItem,
WithCurrencyEffect
} from './interfaces/portfolio-calculator.interface';
import { PortfolioOrder } from './interfaces/portfolio-order.interface';
import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.interface';
import { TransactionPoint } from './interfaces/transaction-point.interface';
export class PortfolioCalculator {
private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT =
true;
private static readonly ENABLE_LOGGING = false;
private currency: string;
private currentRateService: CurrentRateService;
private dataProviderInfos: DataProviderInfo[];
private exchangeRateDataService: ExchangeRateDataService;
private orders: PortfolioOrder[];
private transactionPoints: TransactionPoint[];
public constructor({
currency,
currentRateService,
exchangeRateDataService,
orders
}: {
currency: string;
currentRateService: CurrentRateService;
exchangeRateDataService: ExchangeRateDataService;
orders: PortfolioOrder[];
}) {
this.currency = currency;
this.currentRateService = currentRateService;
this.exchangeRateDataService = exchangeRateDataService;
this.orders = orders;
this.orders.sort((a, b) => {
return a.date?.localeCompare(b.date);
});
}
public computeTransactionPoints() {
this.transactionPoints = [];
const symbols: { [symbol: string]: TransactionPointSymbol } = {};
let lastDate: string = null;
let lastTransactionPoint: TransactionPoint = null;
for (const order of this.orders) {
const currentDate = order.date;
let currentTransactionPointItem: TransactionPointSymbol;
const oldAccumulatedSymbol = symbols[order.symbol];
const factor = this.getFactor(order.type);
const unitPrice = new Big(order.unitPrice);
currentTransactionPointItem = this.getCurrentTransactionPointItem(
oldAccumulatedSymbol,
order,
factor,
unitPrice,
currentTransactionPointItem
);
symbols[order.symbol] = currentTransactionPointItem;
const items = lastTransactionPoint?.items ?? [];
const newItems = items.filter(
(transactionPointItem) => transactionPointItem.symbol !== order.symbol
);
newItems.push(currentTransactionPointItem);
newItems.sort((a, b) => a.symbol?.localeCompare(b.symbol));
if (lastDate !== currentDate || lastTransactionPoint === null) {
lastTransactionPoint = {
date: currentDate,
items: newItems
};
this.transactionPoints.push(lastTransactionPoint);
} else {
lastTransactionPoint.items = newItems;
}
lastDate = currentDate;
}
}
private getCurrentTransactionPointItem(
oldAccumulatedSymbol: TransactionPointSymbol,
order: PortfolioOrder,
factor: number,
unitPrice: Big,
currentTransactionPointItem: TransactionPointSymbol
) {
if (oldAccumulatedSymbol) {
currentTransactionPointItem = this.handleSubsequentTransactions(
order,
factor,
oldAccumulatedSymbol,
unitPrice,
currentTransactionPointItem
);
} else {
currentTransactionPointItem = {
currency: order.currency,
dataSource: order.dataSource,
fee: order.fee,
firstBuyDate: order.date,
investment: unitPrice.mul(order.quantity).mul(factor),
quantity: order.quantity.mul(factor),
symbol: order.symbol,
tags: order.tags,
transactionCount: 1
};
}
return currentTransactionPointItem;
}
private handleSubsequentTransactions(
order: PortfolioOrder,
factor: number,
oldAccumulatedSymbol: TransactionPointSymbol,
unitPrice: Big,
currentTransactionPointItem: TransactionPointSymbol
) {
const newQuantity = order.quantity
.mul(factor)
.plus(oldAccumulatedSymbol.quantity);
let investment = new Big(0);
if (newQuantity.gt(0)) {
if (order.type === 'BUY' || order.type === 'STAKE') {
investment = oldAccumulatedSymbol.investment.plus(
order.quantity.mul(unitPrice)
);
} else if (order.type === 'SELL') {
const averagePrice = oldAccumulatedSymbol.investment.div(
oldAccumulatedSymbol.quantity
);
investment = oldAccumulatedSymbol.investment.minus(
order.quantity.mul(averagePrice)
);
}
}
currentTransactionPointItem = {
investment,
currency: order.currency,
dataSource: order.dataSource,
fee: order.fee.plus(oldAccumulatedSymbol.fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
quantity: newQuantity,
symbol: order.symbol,
tags: order.tags,
transactionCount: oldAccumulatedSymbol.transactionCount + 1
};
return currentTransactionPointItem;
}
public getAnnualizedPerformancePercent({
daysInMarket,
netPerformancePercent
}: {
daysInMarket: number;
netPerformancePercent: Big;
}): Big {
if (isNumber(daysInMarket) && daysInMarket > 0) {
const exponent = new Big(365).div(daysInMarket).toNumber();
return new Big(
Math.pow(netPerformancePercent.plus(1).toNumber(), exponent)
).minus(1);
}
return new Big(0);
}
public getTransactionPoints(): TransactionPoint[] {
return this.transactionPoints;
}
public setTransactionPoints(transactionPoints: TransactionPoint[]) {
this.transactionPoints = transactionPoints;
}
public async getChartData({
start,
end = new Date(Date.now()),
step = 1,
calculateTimeWeightedPerformance = false
}: {
end?: Date;
start: Date;
step?: number;
calculateTimeWeightedPerformance?: boolean;
}): Promise<HistoricalDataItem[]> {
const symbols: { [symbol: string]: boolean } = {};
const transactionPointsBeforeEndDate =
this.transactionPoints?.filter((transactionPoint) => {
return isBefore(parseDate(transactionPoint.date), end);
}) ?? [];
const currencies: { [symbol: string]: string } = {};
const dates: Date[] = [];
const dataGatheringItems: IDataGatheringItem[] = [];
const firstIndex = transactionPointsBeforeEndDate.length;
this.pushDataGatheringsSymbols(
transactionPointsBeforeEndDate,
firstIndex,
dataGatheringItems,
currencies,
symbols
);
this.getRelevantStartAndEndDates(start, end, dates, step);
const dataGartheringDates = [
...dates,
...this.orders
.filter((o) => {
let dateParsed = Date.parse(o.date);
return isBefore(dateParsed, end) && isAfter(dateParsed, start);
})
.map((o) => {
let dateParsed = Date.parse(o.date);
return new Date(dateParsed);
})
];
const { dataProviderInfos, values: marketSymbols } =
await this.getInformationFromCurrentRateService(
currencies,
dataGatheringItems,
dataGartheringDates
);
this.dataProviderInfos = dataProviderInfos;
const marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
} = {};
this.populateMarketSymbolMap(marketSymbols, marketSymbolMap);
const accumulatedValuesByDate: {
[date: string]: {
investmentValueWithCurrencyEffect: Big;
totalCurrentValue: Big;
totalCurrentValueWithCurrencyEffect: Big;
totalInvestmentValue: Big;
totalInvestmentValueWithCurrencyEffect: Big;
totalNetPerformanceValue: Big;
totalNetPerformanceValueWithCurrencyEffect: Big;
totalTimeWeightedInvestmentValue: Big;
totalTimeWeightedInvestmentValueWithCurrencyEffect: Big;
};
} = {};
const valuesBySymbol: {
[symbol: string]: {
currentValues: { [date: string]: Big };
currentValuesWithCurrencyEffect: { [date: string]: Big };
investmentValuesAccumulated: { [date: string]: Big };
investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big };
investmentValuesWithCurrencyEffect: { [date: string]: Big };
netPerformanceValues: { [date: string]: Big };
netPerformanceValuesWithCurrencyEffect: { [date: string]: Big };
timeWeightedInvestmentValues: { [date: string]: Big };
timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big };
netPerformanceValuesPercentage: { [date: string]: Big };
};
} = {};
let exchangeRatesByCurrency =
await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: uniq(Object.values(currencies)),
endDate: endOfDay(end),
startDate: parseDate(this.transactionPoints?.[0]?.date),
targetCurrency: this.currency
});
this.populateSymbolMetrics(
symbols,
end,
marketSymbolMap,
start,
step,
exchangeRatesByCurrency,
valuesBySymbol,
currencies
);
return dates.map((date: Date, index: number, dates: Date[]) => {
let previousDate: Date = index > 0 ? dates[index - 1] : null;
return this.calculatePerformance(
date,
previousDate,
valuesBySymbol,
calculateTimeWeightedPerformance,
accumulatedValuesByDate
);
});
}
private calculatePerformance(
date: Date,
previousDate: Date,
valuesBySymbol: {
[symbol: string]: {
currentValues: { [date: string]: Big };
currentValuesWithCurrencyEffect: { [date: string]: Big };
investmentValuesAccumulated: { [date: string]: Big };
investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big };
investmentValuesWithCurrencyEffect: { [date: string]: Big };
netPerformanceValues: { [date: string]: Big };
netPerformanceValuesWithCurrencyEffect: { [date: string]: Big };
timeWeightedInvestmentValues: { [date: string]: Big };
timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big };
netPerformanceValuesPercentage: { [date: string]: Big };
};
},
calculateTimeWeightedPerformance: boolean,
accumulatedValuesByDate: {
[date: string]: {
investmentValueWithCurrencyEffect: Big;
totalCurrentValue: Big;
totalCurrentValueWithCurrencyEffect: Big;
totalInvestmentValue: Big;
totalInvestmentValueWithCurrencyEffect: Big;
totalNetPerformanceValue: Big;
totalNetPerformanceValueWithCurrencyEffect: Big;
totalTimeWeightedInvestmentValue: Big;
totalTimeWeightedInvestmentValueWithCurrencyEffect: Big;
};
}
) {
const dateString = format(date, DATE_FORMAT);
const previousDateString = previousDate
? format(previousDate, DATE_FORMAT)
: null;
let totalCurrentValue = new Big(0);
let totalInvestmentValue = new Big(0);
let maxTotalInvestmentValue = new Big(0);
let totalNetPerformanceValue = new Big(0);
let previousTotalInvestmentValue = new Big(0);
let timeWeightedPerformance = new Big(0);
if (calculateTimeWeightedPerformance && previousDateString) {
for (const symbol of Object.keys(valuesBySymbol)) {
const symbolValues = valuesBySymbol[symbol];
previousTotalInvestmentValue = previousTotalInvestmentValue.plus(
symbolValues.currentValues?.[previousDateString] ?? new Big(0)
);
}
}
for (const symbol of Object.keys(valuesBySymbol)) {
const symbolValues = valuesBySymbol[symbol];
const symbolCurrentValues =
symbolValues.currentValues?.[dateString] ?? new Big(0);
totalCurrentValue = totalCurrentValue.plus(symbolCurrentValues);
totalNetPerformanceValue = totalNetPerformanceValue.plus(
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0)
);
if (
previousTotalInvestmentValue.toNumber() &&
symbolValues.netPerformanceValuesPercentage &&
(
symbolValues.currentValues?.[previousDateString] ?? new Big(0)
).toNumber()
) {
const previousValue =
symbolValues.currentValues?.[previousDateString] ?? new Big(0);
const netPerformance =
symbolValues.netPerformanceValuesPercentage?.[dateString] ??
new Big(0);
const timeWeightedPerformanceContribution = previousValue
.div(previousTotalInvestmentValue)
.mul(netPerformance)
.mul(100);
timeWeightedPerformance = timeWeightedPerformance.plus(
timeWeightedPerformanceContribution
);
}
this.accumulatedValuesByDate(
valuesBySymbol,
symbol,
dateString,
accumulatedValuesByDate
);
}
const {
investmentValueWithCurrencyEffect,
totalCurrentValueWithCurrencyEffect,
totalInvestmentValueWithCurrencyEffect,
totalNetPerformanceValueWithCurrencyEffect,
totalTimeWeightedInvestmentValue,
totalTimeWeightedInvestmentValueWithCurrencyEffect
} = accumulatedValuesByDate[dateString];
const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0)
? 0
: totalNetPerformanceValue
.div(maxTotalInvestmentValue)
.mul(100)
.toNumber();
const netPerformanceInPercentageWithCurrencyEffect =
totalTimeWeightedInvestmentValueWithCurrencyEffect.eq(0)
? 0
: totalNetPerformanceValueWithCurrencyEffect
.div(totalTimeWeightedInvestmentValueWithCurrencyEffect)
.mul(100)
.toNumber();
return {
date: dateString,
netPerformanceInPercentage,
netPerformanceInPercentageWithCurrencyEffect,
netPerformance: totalNetPerformanceValue.toNumber(),
totalInvestment: totalInvestmentValue.toNumber(),
value: totalCurrentValue.toNumber(),
valueWithCurrencyEffect: totalCurrentValueWithCurrencyEffect.toNumber(),
timeWeightedPerformance: timeWeightedPerformance.toNumber(),
investmentValueWithCurrencyEffect:
investmentValueWithCurrencyEffect.toNumber(),
netPerformanceWithCurrencyEffect:
totalNetPerformanceValueWithCurrencyEffect.toNumber(),
totalInvestmentValueWithCurrencyEffect:
totalInvestmentValueWithCurrencyEffect.toNumber()
};
}
private accumulatedValuesByDate(
valuesBySymbol: {
[symbol: string]: {
currentValues: { [date: string]: Big };
currentValuesWithCurrencyEffect: { [date: string]: Big };
investmentValuesAccumulated: { [date: string]: Big };
investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big };
investmentValuesWithCurrencyEffect: { [date: string]: Big };
netPerformanceValues: { [date: string]: Big };
netPerformanceValuesWithCurrencyEffect: { [date: string]: Big };
timeWeightedInvestmentValues: { [date: string]: Big };
timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big };
netPerformanceValuesPercentage: { [date: string]: Big };
};
},
symbol: string,
dateString: string,
accumulatedValuesByDate: {
[date: string]: {
investmentValueWithCurrencyEffect: Big;
totalCurrentValue: Big;
totalCurrentValueWithCurrencyEffect: Big;
totalInvestmentValue: Big;
totalInvestmentValueWithCurrencyEffect: Big;
totalNetPerformanceValue: Big;
totalNetPerformanceValueWithCurrencyEffect: Big;
totalTimeWeightedInvestmentValue: Big;
totalTimeWeightedInvestmentValueWithCurrencyEffect: Big;
};
}
) {
const symbolValues = valuesBySymbol[symbol];
const currentValue = symbolValues.currentValues?.[dateString] ?? new Big(0);
const currentValueWithCurrencyEffect =
symbolValues.currentValuesWithCurrencyEffect?.[dateString] ?? new Big(0);
const investmentValueAccumulated =
symbolValues.investmentValuesAccumulated?.[dateString] ?? new Big(0);
const investmentValueAccumulatedWithCurrencyEffect =
symbolValues.investmentValuesAccumulatedWithCurrencyEffect?.[
dateString
] ?? new Big(0);
const investmentValueWithCurrencyEffect =
symbolValues.investmentValuesWithCurrencyEffect?.[dateString] ??
new Big(0);
const netPerformanceValue =
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0);
const netPerformanceValueWithCurrencyEffect =
symbolValues.netPerformanceValuesWithCurrencyEffect?.[dateString] ??
new Big(0);
const timeWeightedInvestmentValue =
symbolValues.timeWeightedInvestmentValues?.[dateString] ?? new Big(0);
const timeWeightedInvestmentValueWithCurrencyEffect =
symbolValues.timeWeightedInvestmentValuesWithCurrencyEffect?.[
dateString
] ?? new Big(0);
accumulatedValuesByDate[dateString] = {
investmentValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString]
?.investmentValueWithCurrencyEffect ?? new Big(0)
).add(investmentValueWithCurrencyEffect),
totalCurrentValue: (
accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
).add(currentValue),
totalCurrentValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString]
?.totalCurrentValueWithCurrencyEffect ?? new Big(0)
).add(currentValueWithCurrencyEffect),
totalInvestmentValue: (
accumulatedValuesByDate[dateString]?.totalInvestmentValue ?? new Big(0)
).add(investmentValueAccumulated),
totalInvestmentValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString]
?.totalInvestmentValueWithCurrencyEffect ?? new Big(0)
).add(investmentValueAccumulatedWithCurrencyEffect),
totalNetPerformanceValue: (
accumulatedValuesByDate[dateString]?.totalNetPerformanceValue ??
new Big(0)
).add(netPerformanceValue),
totalNetPerformanceValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString]
?.totalNetPerformanceValueWithCurrencyEffect ?? new Big(0)
).add(netPerformanceValueWithCurrencyEffect),
totalTimeWeightedInvestmentValue: (
accumulatedValuesByDate[dateString]?.totalTimeWeightedInvestmentValue ??
new Big(0)
).add(timeWeightedInvestmentValue),
totalTimeWeightedInvestmentValueWithCurrencyEffect: (
accumulatedValuesByDate[dateString]
?.totalTimeWeightedInvestmentValueWithCurrencyEffect ?? new Big(0)
).add(timeWeightedInvestmentValueWithCurrencyEffect)
};
}
private populateSymbolMetrics(
symbols: { [symbol: string]: boolean },
end: Date,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
start: Date,
step: number,
exchangeRatesByCurrency,
valuesBySymbol: {
[symbol: string]: {
currentValues: { [date: string]: Big };
currentValuesWithCurrencyEffect: { [date: string]: Big };
investmentValuesAccumulated: { [date: string]: Big };
investmentValuesAccumulatedWithCurrencyEffect: { [date: string]: Big };
investmentValuesWithCurrencyEffect: { [date: string]: Big };
netPerformanceValues: { [date: string]: Big };
netPerformanceValuesWithCurrencyEffect: { [date: string]: Big };
timeWeightedInvestmentValues: { [date: string]: Big };
timeWeightedInvestmentValuesWithCurrencyEffect: { [date: string]: Big };
netPerformanceValuesPercentage: { [date: string]: Big };
};
},
currencies: { [symbol: string]: string }
) {
for (const symbol of Object.keys(symbols)) {
const {
currentValues,
currentValuesWithCurrencyEffect,
investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect,
netPerformanceValues,
netPerformanceValuesWithCurrencyEffect,
timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect,
netPerformanceValuesPercentage
} = this.getSymbolMetrics({
end,
marketSymbolMap,
start,
step,
symbol,
exchangeRates:
exchangeRatesByCurrency[`${currencies[symbol]}${this.currency}`],
isChartMode: true
});
valuesBySymbol[symbol] = {
currentValues,
currentValuesWithCurrencyEffect,
investmentValuesAccumulated,
investmentValuesAccumulatedWithCurrencyEffect,
investmentValuesWithCurrencyEffect,
netPerformanceValues,
netPerformanceValuesWithCurrencyEffect,
timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect,
netPerformanceValuesPercentage
};
}
}
private populateMarketSymbolMap(
marketSymbols: GetValueObject[],
marketSymbolMap: { [date: string]: { [symbol: string]: Big } }
) {
for (const marketSymbol of marketSymbols) {
const dateString = format(marketSymbol.date, DATE_FORMAT);
if (!marketSymbolMap[dateString]) {
marketSymbolMap[dateString] = {};
}
if (marketSymbol.marketPrice) {
marketSymbolMap[dateString][marketSymbol.symbol] = new Big(
marketSymbol.marketPrice
);
}
}
}
private async getInformationFromCurrentRateService(
currencies: { [symbol: string]: string },
dataGatheringItems: IDataGatheringItem[],
dates: Date[]
): Promise<{
dataProviderInfos: DataProviderInfo[];
values: GetValueObject[];
}> {
return await this.currentRateService.getValues({
dataGatheringItems,
dateQuery: {
in: dates
}
});
}
private pushDataGatheringsSymbols(
transactionPointsBeforeEndDate: TransactionPoint[],
firstIndex: number,
dataGatheringItems: IDataGatheringItem[],
currencies: { [symbol: string]: string },
symbols: { [symbol: string]: boolean }
) {
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
dataGatheringItems.push({
dataSource: item.dataSource,
symbol: item.symbol
});
currencies[item.symbol] = item.currency;
symbols[item.symbol] = true;
}
}
private getRelevantStartAndEndDates(
start: Date,
end: Date,
dates: Date[],
step: number
) {
let day = start;
while (isBefore(day, end)) {
dates.push(resetHours(day));
day = addDays(day, step);
}
if (!isSameDay(last(dates), end)) {
dates.push(resetHours(end));
}
}
public async getCurrentPositions(
start: Date,
end = new Date(Date.now())
): Promise<CurrentPositions> {
const transactionPointsBeforeEndDate =
this.transactionPoints?.filter((transactionPoint) => {
return isBefore(parseDate(transactionPoint.date), end);
}) ?? [];
if (!transactionPointsBeforeEndDate.length) {
return {
currentValue: new Big(0),
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
grossPerformanceWithCurrencyEffect: new Big(0),
hasErrors: false,
netPerformance: new Big(0),
netPerformancePercentage: new Big(0),
netPerformancePercentageWithCurrencyEffect: new Big(0),
netPerformanceWithCurrencyEffect: new Big(0),
positions: [],
totalInvestment: new Big(0)
};
}
const lastTransactionPoint =
transactionPointsBeforeEndDate[transactionPointsBeforeEndDate.length - 1];
const currencies: { [symbol: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = [];
let dates: Date[] = [];
let firstIndex = transactionPointsBeforeEndDate.length;
let firstTransactionPoint: TransactionPoint = null;
dates.push(resetHours(start));
for (const item of transactionPointsBeforeEndDate[firstIndex - 1].items) {
dataGatheringItems.push({
dataSource: item.dataSource,
symbol: item.symbol
});
currencies[item.symbol] = item.currency;
}
for (let i = 0; i < transactionPointsBeforeEndDate.length; i++) {
if (
!isBefore(parseDate(transactionPointsBeforeEndDate[i].date), start) &&
firstTransactionPoint === null
) {
firstTransactionPoint = transactionPointsBeforeEndDate[i];
firstIndex = i;
}
if (firstTransactionPoint !== null) {
dates.push(
resetHours(parseDate(transactionPointsBeforeEndDate[i].date))
);
}
}
dates.push(resetHours(end));
// Add dates of last week for fallback
dates.push(subDays(resetHours(new Date()), 7));
dates.push(subDays(resetHours(new Date()), 6));
dates.push(subDays(resetHours(new Date()), 5));
dates.push(subDays(resetHours(new Date()), 4));
dates.push(subDays(resetHours(new Date()), 3));
dates.push(subDays(resetHours(new Date()), 2));
dates.push(subDays(resetHours(new Date()), 1));
dates.push(resetHours(new Date()));
dates = uniq(
dates.map((date) => {
return date.getTime();
})
)
.map((timestamp) => {
return new Date(timestamp);
})
.sort((a, b) => {
return a.getTime() - b.getTime();
});
let exchangeRatesByCurrency =
await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: uniq(Object.values(currencies)),
endDate: endOfDay(end),
startDate: parseDate(this.transactionPoints?.[0]?.date),
targetCurrency: this.currency
});
const {
dataProviderInfos,
errors: currentRateErrors,
values: marketSymbols
} = await this.currentRateService.getValues({
dataGatheringItems,
dateQuery: {
in: dates
}
});
this.dataProviderInfos = dataProviderInfos;
const marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
} = {};
for (const marketSymbol of marketSymbols) {
const date = format(marketSymbol.date, DATE_FORMAT);
if (!marketSymbolMap[date]) {
marketSymbolMap[date] = {};
}
if (marketSymbol.marketPrice) {
marketSymbolMap[date][marketSymbol.symbol] = new Big(
marketSymbol.marketPrice
);
}
}
const endDateString = format(end, DATE_FORMAT);
if (firstIndex > 0) {
firstIndex--;
}
const initialValues: { [symbol: string]: Big } = {};
const positions: TimelinePosition[] = [];
let hasAnySymbolMetricsErrors = false;
const errors: ResponseError['errors'] = [];
for (const item of lastTransactionPoint.items) {
const marketPriceInBaseCurrency = marketSymbolMap[endDateString]?.[
item.symbol
]?.mul(
exchangeRatesByCurrency[`${item.currency}${this.currency}`]?.[
endDateString
]
);
const {
grossPerformance,
grossPerformancePercentage,
grossPerformancePercentageWithCurrencyEffect,
grossPerformanceWithCurrencyEffect,
hasErrors,
initialValue,
netPerformance,
netPerformancePercentage,
netPerformancePercentageWithCurrencyEffect,
netPerformanceWithCurrencyEffect,
timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect,
totalInvestment,
totalInvestmentWithCurrencyEffect
} = this.getSymbolMetrics({
end,
marketSymbolMap,
start,
exchangeRates:
exchangeRatesByCurrency[`${item.currency}${this.currency}`],
symbol: item.symbol
});
hasAnySymbolMetricsErrors = hasAnySymbolMetricsErrors || hasErrors;
initialValues[item.symbol] = initialValue;
positions.push({
timeWeightedInvestment,
timeWeightedInvestmentWithCurrencyEffect,
averagePrice: item.quantity.eq(0)
? new Big(0)
: item.investment.div(item.quantity),
currency: item.currency,
dataSource: item.dataSource,
fee: item.fee,
firstBuyDate: item.firstBuyDate,
grossPerformance: !hasErrors ? grossPerformance ?? null : null,
grossPerformancePercentage: !hasErrors
? grossPerformancePercentage ?? null
: null,
grossPerformancePercentageWithCurrencyEffect: !hasErrors
? grossPerformancePercentageWithCurrencyEffect ?? null
: null,
grossPerformanceWithCurrencyEffect: !hasErrors
? grossPerformanceWithCurrencyEffect ?? null
: null,
investment: totalInvestment,
investmentWithCurrencyEffect: totalInvestmentWithCurrencyEffect,
marketPrice:
marketSymbolMap[endDateString]?.[item.symbol]?.toNumber() ?? null,
marketPriceInBaseCurrency:
marketPriceInBaseCurrency?.toNumber() ?? null,
netPerformance: !hasErrors ? netPerformance ?? null : null,
netPerformancePercentage: !hasErrors
? netPerformancePercentage ?? null
: null,
netPerformancePercentageWithCurrencyEffect: !hasErrors
? netPerformancePercentageWithCurrencyEffect ?? null
: null,
netPerformanceWithCurrencyEffect: !hasErrors
? netPerformanceWithCurrencyEffect ?? null
: null,
quantity: item.quantity,
symbol: item.symbol,
tags: item.tags,
transactionCount: item.transactionCount
});
if (
(hasErrors ||
currentRateErrors.find(({ dataSource, symbol }) => {
return dataSource === item.dataSource && symbol === item.symbol;
})) &&
item.investment.gt(0)
) {
errors.push({ dataSource: item.dataSource, symbol: item.symbol });
}
}
const overall = this.calculateOverallPerformance(positions);
return {
...overall,
errors,
positions,
hasErrors: hasAnySymbolMetricsErrors || overall.hasErrors
};
}
public getDataProviderInfos() {
return this.dataProviderInfos;
}
public getInvestments(): { date: string; investment: Big }[] {
if (this.transactionPoints.length === 0) {
return [];
}
return this.transactionPoints.map((transactionPoint) => {
return {
date: transactionPoint.date,
investment: transactionPoint.items.reduce(
(investment, transactionPointSymbol) =>
investment.plus(transactionPointSymbol.investment),
new Big(0)
)
};
});
}
public getInvestmentsByGroup({
data,
groupBy
}: {
data: HistoricalDataItem[];
groupBy: GroupBy;
}): InvestmentItem[] {
const groupedData: { [dateGroup: string]: Big } = {};
for (const { date, investmentValueWithCurrencyEffect } of data) {
const dateGroup =
groupBy === 'month' ? date.substring(0, 7) : date.substring(0, 4);
groupedData[dateGroup] = (groupedData[dateGroup] ?? new Big(0)).plus(
investmentValueWithCurrencyEffect
);
}
return Object.keys(groupedData).map((dateGroup) => ({
date: groupBy === 'month' ? `${dateGroup}-01` : `${dateGroup}-01-01`,
investment: groupedData[dateGroup].toNumber()
}));
}
private calculateOverallPerformance(positions: TimelinePosition[]) {
let currentValue = new Big(0);
let grossPerformance = new Big(0);
let grossPerformanceWithCurrencyEffect = new Big(0);
let hasErrors = false;
let netPerformance = new Big(0);
let netPerformanceWithCurrencyEffect = new Big(0);
let totalInvestment = new Big(0);
let totalInvestmentWithCurrencyEffect = new Big(0);
let totalTimeWeightedInvestment = new Big(0);
let totalTimeWeightedInvestmentWithCurrencyEffect = new Big(0);
for (const currentPosition of positions) {
if (
currentPosition.investment &&
currentPosition.marketPriceInBaseCurrency
) {
currentValue = currentValue.plus(
new Big(currentPosition.marketPriceInBaseCurrency).mul(
currentPosition.quantity
)
);
} else {
hasErrors = true;
}
if (currentPosition.investment) {
totalInvestment = totalInvestment.plus(currentPosition.investment);
totalInvestmentWithCurrencyEffect =
totalInvestmentWithCurrencyEffect.plus(
currentPosition.investmentWithCurrencyEffect
);
} else {
hasErrors = true;
}
if (currentPosition.grossPerformance) {
grossPerformance = grossPerformance.plus(
currentPosition.grossPerformance
);
grossPerformanceWithCurrencyEffect =
grossPerformanceWithCurrencyEffect.plus(
currentPosition.grossPerformanceWithCurrencyEffect
);
netPerformance = netPerformance.plus(currentPosition.netPerformance);
netPerformanceWithCurrencyEffect =
netPerformanceWithCurrencyEffect.plus(
currentPosition.netPerformanceWithCurrencyEffect
);
} else if (!currentPosition.quantity.eq(0)) {
hasErrors = true;
}
if (currentPosition.timeWeightedInvestment) {
totalTimeWeightedInvestment = totalTimeWeightedInvestment.plus(
currentPosition.timeWeightedInvestment
);
totalTimeWeightedInvestmentWithCurrencyEffect =
totalTimeWeightedInvestmentWithCurrencyEffect.plus(
currentPosition.timeWeightedInvestmentWithCurrencyEffect
);
} else if (!currentPosition.quantity.eq(0)) {
Logger.warn(
`Missing historical market data for ${currentPosition.symbol} (${currentPosition.dataSource})`,
'PortfolioCalculator'
);
hasErrors = true;
}
}
return {
currentValue,
grossPerformance,
grossPerformanceWithCurrencyEffect,
hasErrors,
netPerformance,
netPerformanceWithCurrencyEffect,
totalInvestment,
totalInvestmentWithCurrencyEffect,
netPerformancePercentage: totalTimeWeightedInvestment.eq(0)
? new Big(0)
: netPerformance.div(totalTimeWeightedInvestment),
netPerformancePercentageWithCurrencyEffect:
totalTimeWeightedInvestmentWithCurrencyEffect.eq(0)
? new Big(0)
: netPerformanceWithCurrencyEffect.div(
totalTimeWeightedInvestmentWithCurrencyEffect
),
grossPerformancePercentage: totalTimeWeightedInvestment.eq(0)
? new Big(0)
: grossPerformance.div(totalTimeWeightedInvestment),
grossPerformancePercentageWithCurrencyEffect:
totalTimeWeightedInvestmentWithCurrencyEffect.eq(0)
? new Big(0)
: grossPerformanceWithCurrencyEffect.div(
totalTimeWeightedInvestmentWithCurrencyEffect
)
};
}
private getFactor(type: TypeOfOrder) {
let factor: number;
switch (type) {
case 'BUY':
case 'STAKE':
factor = 1;
break;
case 'SELL':
factor = -1;
break;
default:
factor = 0;
break;
}
return factor;
}
private getSymbolMetrics({
end,
exchangeRates,
isChartMode = false,
marketSymbolMap,
start,
step = 1,
symbol
}: {
end: Date;
exchangeRates: { [dateString: string]: number };
isChartMode?: boolean;
marketSymbolMap: {
[date: string]: { [symbol: string]: Big };
};
start: Date;
step?: number;
symbol: string;
}): SymbolMetrics {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const currentValues: WithCurrencyEffect<{ [date: string]: Big }> = {
Value: {},
WithCurrencyEffect: {}
};
let fees: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let feesAtStartDate: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let grossPerformance: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let grossPerformanceAtStartDate: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let grossPerformanceFromSells: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let averagePriceAtEndDate = new Big(0);
let averagePriceAtStartDate = new Big(0);
const investmentValues: WithCurrencyEffect<{ [date: string]: Big }> = {
Value: {},
WithCurrencyEffect: {}
};
const maxInvestmentValues: { [date: string]: Big } = {};
let maxTotalInvestment = new Big(0);
const netPerformanceValuesPercentage: { [date: string]: Big } = {};
let initialValue: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let investmentAtStartDate: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
const investmentValuesAccumulated: WithCurrencyEffect<{
[date: string]: Big;
}> = {
Value: {},
WithCurrencyEffect: {}
};
let lastAveragePrice: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
const netPerformanceValues: WithCurrencyEffect<{ [date: string]: Big }> = {
Value: {},
WithCurrencyEffect: {}
};
const timeWeightedInvestmentValues: WithCurrencyEffect<{
[date: string]: Big;
}> = {
Value: {},
WithCurrencyEffect: {}
};
let totalInvestment: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let totalInvestmentWithGrossPerformanceFromSell: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
let totalUnits = new Big(0);
let valueAtStartDate: WithCurrencyEffect<Big> = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
// Clone orders to keep the original values in this.orders
let orders: PortfolioOrderItem[] = cloneDeep(this.orders).filter(
(order) => {
return order.symbol === symbol;
}
);
if (orders.length <= 0) {
return {
currentValues: {},
currentValuesWithCurrencyEffect: {},
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
grossPerformanceWithCurrencyEffect: new Big(0),
hasErrors: false,
initialValue: new Big(0),
initialValueWithCurrencyEffect: new Big(0),
investmentValuesAccumulated: {},
investmentValuesAccumulatedWithCurrencyEffect: {},
investmentValuesWithCurrencyEffect: {},
netPerformance: new Big(0),
netPerformancePercentage: new Big(0),
netPerformancePercentageWithCurrencyEffect: new Big(0),
netPerformanceValues: {},
netPerformanceValuesWithCurrencyEffect: {},
netPerformanceWithCurrencyEffect: new Big(0),
timeWeightedInvestment: new Big(0),
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
timeWeightedInvestmentValues: {},
timeWeightedInvestmentValuesWithCurrencyEffect: {},
totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0),
netPerformanceValuesPercentage: {}
};
}
const dateOfFirstTransaction = new Date(first(orders).date);
const unitPriceAtStartDate =
marketSymbolMap[format(start, DATE_FORMAT)]?.[symbol];
const unitPriceAtEndDate =
marketSymbolMap[format(end, DATE_FORMAT)]?.[symbol];
if (
!unitPriceAtEndDate ||
(!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start))
) {
return {
currentValues: {},
currentValuesWithCurrencyEffect: {},
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
grossPerformancePercentageWithCurrencyEffect: new Big(0),
grossPerformanceWithCurrencyEffect: new Big(0),
hasErrors: true,
initialValue: new Big(0),
initialValueWithCurrencyEffect: new Big(0),
investmentValuesAccumulated: {},
investmentValuesAccumulatedWithCurrencyEffect: {},
investmentValuesWithCurrencyEffect: {},
netPerformance: new Big(0),
netPerformancePercentage: new Big(0),
netPerformancePercentageWithCurrencyEffect: new Big(0),
netPerformanceValues: {},
netPerformanceValuesWithCurrencyEffect: {},
netPerformanceWithCurrencyEffect: new Big(0),
timeWeightedInvestment: new Big(0),
timeWeightedInvestmentValues: {},
timeWeightedInvestmentValuesWithCurrencyEffect: {},
timeWeightedInvestmentWithCurrencyEffect: new Big(0),
totalInvestment: new Big(0),
totalInvestmentWithCurrencyEffect: new Big(0),
netPerformanceValuesPercentage: {}
};
}
// Add a synthetic order at the start and the end date
this.addSyntheticStartAndEndOrders(
orders,
symbol,
start,
unitPriceAtStartDate,
end,
unitPriceAtEndDate
);
let day = start;
let lastUnitPrice: Big;
({ day, lastUnitPrice } = this.handleChartMode(
isChartMode,
orders,
day,
end,
symbol,
marketSymbolMap,
lastUnitPrice,
step
));
// Sort orders so that the start and end placeholder order are at the right
// position
orders = this.sortOrdersByTime(orders);
const indexOfStartOrder = orders.findIndex((order) => {
return order.itemType === 'start';
});
const indexOfEndOrder = orders.findIndex((order) => {
return order.itemType === 'end';
});
const result = this.calculatePerformanceOfSymbol(
orders,
indexOfStartOrder,
unitPriceAtStartDate,
averagePriceAtStartDate,
totalUnits,
totalInvestment,
investmentAtStartDate,
valueAtStartDate,
maxTotalInvestment,
indexOfEndOrder,
averagePriceAtEndDate,
initialValue,
marketSymbolMap,
fees,
feesAtStartDate,
lastAveragePrice,
grossPerformanceFromSells,
totalInvestmentWithGrossPerformanceFromSell,
grossPerformance,
grossPerformanceAtStartDate,
isChartMode,
currentValues,
netPerformanceValues,
netPerformanceValuesPercentage,
investmentValues,
investmentValuesAccumulated,
maxInvestmentValues,
timeWeightedInvestmentValues,
unitPriceAtEndDate,
symbol,
exchangeRates,
currentExchangeRate
);
return {
currentValues: result.currentValues.Value,
currentValuesWithCurrencyEffect: result.currentValues.WithCurrencyEffect,
grossPerformancePercentage: result.grossPerformancePercentage.Value,
grossPerformancePercentageWithCurrencyEffect:
result.grossPerformancePercentage.WithCurrencyEffect,
initialValue: result.initialValue.Value,
initialValueWithCurrencyEffect: result.initialValue.WithCurrencyEffect,
investmentValuesWithCurrencyEffect:
result.investmentValues.WithCurrencyEffect,
netPerformancePercentage: result.netPerformancePercentage.Value,
netPerformancePercentageWithCurrencyEffect:
result.netPerformancePercentage.WithCurrencyEffect,
netPerformanceValues: result.netPerformanceValues.Value,
netPerformanceValuesWithCurrencyEffect:
result.netPerformanceValues.WithCurrencyEffect,
grossPerformance: result.grossPerformance.Value,
grossPerformanceWithCurrencyEffect:
result.grossPerformance.WithCurrencyEffect,
hasErrors: result.hasErrors,
netPerformance: result.netPerformance.Value,
netPerformanceWithCurrencyEffect:
result.netPerformance.WithCurrencyEffect,
totalInvestment: result.totalInvestment.Value,
totalInvestmentWithCurrencyEffect:
result.totalInvestment.WithCurrencyEffect,
netPerformanceValuesPercentage: result.netPerformanceValuesPercentage,
investmentValuesAccumulated: result.investmentValuesAccumulated.Value,
investmentValuesAccumulatedWithCurrencyEffect:
result.investmentValuesAccumulated.WithCurrencyEffect,
timeWeightedInvestmentValues: result.timeWeightedInvestmentValues.Value,
timeWeightedInvestmentValuesWithCurrencyEffect:
result.timeWeightedInvestmentValues.WithCurrencyEffect,
timeWeightedInvestment:
result.timeWeightedAverageInvestmentBetweenStartAndEndDate.Value,
timeWeightedInvestmentWithCurrencyEffect:
result.timeWeightedAverageInvestmentBetweenStartAndEndDate
.WithCurrencyEffect
};
}
private calculatePerformanceOfSymbol(
orders: PortfolioOrderItem[],
indexOfStartOrder: number,
unitPriceAtStartDate: Big,
averagePriceAtStartDate: Big,
totalUnits: Big,
totalInvestment: WithCurrencyEffect<Big>,
investmentAtStartDate: WithCurrencyEffect<Big>,
valueAtStartDate: WithCurrencyEffect<Big>,
maxTotalInvestment: Big,
indexOfEndOrder: number,
averagePriceAtEndDate: Big,
initialValue: WithCurrencyEffect<Big>,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
fees: WithCurrencyEffect<Big>,
feesAtStartDate: WithCurrencyEffect<Big>,
lastAveragePrice: WithCurrencyEffect<Big>,
grossPerformanceFromSells: WithCurrencyEffect<Big>,
totalInvestmentWithGrossPerformanceFromSell: WithCurrencyEffect<Big>,
grossPerformance: WithCurrencyEffect<Big>,
grossPerformanceAtStartDate: WithCurrencyEffect<Big>,
isChartMode: boolean,
currentValues: WithCurrencyEffect<{ [date: string]: Big }>,
netPerformanceValues: WithCurrencyEffect<{ [date: string]: Big }>,
netPerformanceValuesPercentage: { [date: string]: Big },
investmentValues: WithCurrencyEffect<{ [date: string]: Big }>,
investmentValuesAccumulated: WithCurrencyEffect<{ [date: string]: Big }>,
maxInvestmentValues: { [date: string]: Big },
timeWeightedInvestmentValues: WithCurrencyEffect<{ [date: string]: Big }>,
unitPriceAtEndDate: Big,
symbol: string,
exchangeRates: { [dateString: string]: number },
currentExchangeRate: number
) {
let totalInvestmentDays = 0;
let sumOfTimeWeightedInvestments = {
Value: new Big(0),
WithCurrencyEffect: new Big(0)
};
({
lastAveragePrice,
grossPerformance,
feesAtStartDate,
grossPerformanceAtStartDate,
averagePriceAtStartDate,
totalUnits,
totalInvestment,
investmentAtStartDate,
valueAtStartDate,
maxTotalInvestment,
averagePriceAtEndDate,
initialValue,
fees,
netPerformanceValuesPercentage,
totalInvestmentDays
} = this.handleOrders(
orders,
indexOfStartOrder,
unitPriceAtStartDate,
averagePriceAtStartDate,
totalUnits,
totalInvestment,
investmentAtStartDate,
valueAtStartDate,
maxTotalInvestment,
averagePriceAtEndDate,
initialValue,
fees,
feesAtStartDate,
indexOfEndOrder,
marketSymbolMap,
grossPerformanceFromSells,
totalInvestmentWithGrossPerformanceFromSell,
lastAveragePrice,
grossPerformance,
grossPerformanceAtStartDate,
isChartMode,
currentValues,
netPerformanceValues,
netPerformanceValuesPercentage,
investmentValues,
investmentValuesAccumulated,
totalInvestmentDays,
sumOfTimeWeightedInvestments,
timeWeightedInvestmentValues,
exchangeRates,
currentExchangeRate
));
const totalGrossPerformance = {
Value: grossPerformance.Value.minus(grossPerformanceAtStartDate.Value),
WithCurrencyEffect: grossPerformance.WithCurrencyEffect.minus(
grossPerformanceAtStartDate.WithCurrencyEffect
)
};
const totalNetPerformance = {
Value: grossPerformance.Value.minus(
grossPerformanceAtStartDate.Value
).minus(fees.Value.minus(feesAtStartDate.Value)),
WithCurrencyEffect: grossPerformance.WithCurrencyEffect.minus(
grossPerformanceAtStartDate.WithCurrencyEffect
).minus(fees.WithCurrencyEffect.minus(feesAtStartDate.WithCurrencyEffect))
};
const maxInvestmentBetweenStartAndEndDate = valueAtStartDate.Value.plus(
maxTotalInvestment.minus(investmentAtStartDate.Value)
);
const timeWeightedAverageInvestmentBetweenStartAndEndDate = {
Value:
totalInvestmentDays > 0
? sumOfTimeWeightedInvestments.Value.div(totalInvestmentDays)
: new Big(0),
WithCurrencyEffect:
totalInvestmentDays > 0
? sumOfTimeWeightedInvestments.WithCurrencyEffect.div(
totalInvestmentDays
)
: new Big(0)
};
const grossPerformancePercentage = {
Value: this.calculateGrossPerformancePercentage(
averagePriceAtStartDate,
averagePriceAtEndDate,
orders,
indexOfStartOrder,
maxInvestmentBetweenStartAndEndDate,
totalGrossPerformance.Value,
unitPriceAtEndDate
),
WithCurrencyEffect:
timeWeightedAverageInvestmentBetweenStartAndEndDate.WithCurrencyEffect.gt(
0
)
? totalGrossPerformance.WithCurrencyEffect.div(
timeWeightedAverageInvestmentBetweenStartAndEndDate.WithCurrencyEffect
)
: new Big(0)
};
const feesPerUnit = {
Value: totalUnits.gt(0)
? fees.Value.minus(feesAtStartDate.Value).div(totalUnits)
: new Big(0),
WithCurrencyEffect: totalUnits.gt(0)
? fees.WithCurrencyEffect.minus(feesAtStartDate.WithCurrencyEffect).div(
totalUnits
)
: new Big(0)
};
const netPerformancePercentage = this.calculateNetPerformancePercentage(
timeWeightedAverageInvestmentBetweenStartAndEndDate,
totalNetPerformance
);
this.handleLogging(
symbol,
orders,
indexOfStartOrder,
unitPriceAtEndDate,
totalInvestment,
totalGrossPerformance,
grossPerformancePercentage,
feesPerUnit,
totalNetPerformance,
netPerformancePercentage
);
return {
currentValues,
grossPerformancePercentage,
initialValue,
investmentValues,
maxInvestmentValues,
netPerformancePercentage,
netPerformanceValues,
grossPerformance: totalGrossPerformance,
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
netPerformance: totalNetPerformance,
averagePriceAtStartDate,
totalUnits,
totalInvestment,
investmentAtStartDate,
valueAtStartDate,
maxTotalInvestment,
averagePriceAtEndDate,
fees,
lastAveragePrice,
grossPerformanceFromSells,
totalInvestmentWithGrossPerformanceFromSell,
feesAtStartDate,
grossPerformanceAtStartDate,
netPerformanceValuesPercentage,
investmentValuesAccumulated,
timeWeightedInvestmentValues,
timeWeightedAverageInvestmentBetweenStartAndEndDate
};
}
private handleOrders(
orders: PortfolioOrderItem[],
indexOfStartOrder: number,
unitPriceAtStartDate: Big,
averagePriceAtStartDate: Big,
totalUnits: Big,
totalInvestment: WithCurrencyEffect<Big>,
investmentAtStartDate: WithCurrencyEffect<Big>,
valueAtStartDate: WithCurrencyEffect<Big>,
maxTotalInvestment: Big,
averagePriceAtEndDate: Big,
initialValue: WithCurrencyEffect<Big>,
fees: WithCurrencyEffect<Big>,
feesAtStartDate: WithCurrencyEffect<Big>,
indexOfEndOrder: number,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
grossPerformanceFromSells: WithCurrencyEffect<Big>,
totalInvestmentWithGrossPerformanceFromSell: WithCurrencyEffect<Big>,
lastAveragePrice: WithCurrencyEffect<Big>,
grossPerformance: WithCurrencyEffect<Big>,
grossPerformanceAtStartDate: WithCurrencyEffect<Big>,
isChartMode: boolean,
currentValues: WithCurrencyEffect<{ [date: string]: Big }>,
netPerformanceValues: WithCurrencyEffect<{ [date: string]: Big }>,
netPerformanceValuesPercentage: { [date: string]: Big },
investmentValues: WithCurrencyEffect<{ [date: string]: Big }>,
investmentValuesAccumulated: WithCurrencyEffect<{ [date: string]: Big }>,
totalInvestmentDays: number,
sumOfTimeWeightedInvestments: WithCurrencyEffect<Big>,
timeWeightedInvestmentValues: WithCurrencyEffect<{ [date: string]: Big }>,
exchangeRates: { [dateString: string]: number },
currentExchangeRate: number
) {
for (let i = 0; i < orders.length; i += 1) {
const order = orders[i];
const previousOrderDateString = i > 0 ? orders[i - 1].date : '';
this.calculateNetPerformancePercentageForDateAndSymbol(
i,
orders,
order,
netPerformanceValuesPercentage,
marketSymbolMap
);
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log();
console.log();
console.log(i + 1, order.type, order.itemType);
}
this.handleStartOrder(
order,
indexOfStartOrder,
orders,
i,
unitPriceAtStartDate
);
const exchangeRateAtOrderDate = exchangeRates[order.date];
this.handleFeeAndUnitPriceOfOrder(
order,
currentExchangeRate,
exchangeRateAtOrderDate
);
// Calculate the average start price as soon as any units are held
let transactionInvestment: WithCurrencyEffect<Big>;
let totalInvestmentBeforeTransaction: WithCurrencyEffect<Big>;
let valueOfInvestment: WithCurrencyEffect<Big>;
let valueOfInvestmentBeforeTransaction: WithCurrencyEffect<Big>;
({
transactionInvestment,
valueOfInvestment,
averagePriceAtStartDate,
totalUnits,
totalInvestment,
investmentAtStartDate,
valueAtStartDate,
maxTotalInvestment,
averagePriceAtEndDate,
initialValue,
fees,
totalInvestmentBeforeTransaction,
valueOfInvestmentBeforeTransaction
} = this.calculateInvestmentSpecificMetrics(
averagePriceAtStartDate,
i,
indexOfStartOrder,
totalUnits,
totalInvestment,
order,
investmentAtStartDate,
valueAtStartDate,
maxTotalInvestment,
averagePriceAtEndDate,
indexOfEndOrder,
initialValue,
marketSymbolMap,
fees
));
({
grossPerformanceFromSells,
totalInvestmentWithGrossPerformanceFromSell
} = this.calculateSellOrders(
order,
lastAveragePrice,
grossPerformanceFromSells,
totalInvestmentWithGrossPerformanceFromSell,
transactionInvestment
));
lastAveragePrice.Value = totalUnits.eq(0)
? new Big(0)
: totalInvestmentWithGrossPerformanceFromSell.Value.div(totalUnits);
lastAveragePrice.WithCurrencyEffect = totalUnits.eq(0)
? new Big(0)
: totalInvestmentWithGrossPerformanceFromSell.WithCurrencyEffect.div(
totalUnits
);
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log(
'totalInvestmentWithGrossPerformanceFromSell',
totalInvestmentWithGrossPerformanceFromSell.Value.toNumber()
);
console.log(
'totalInvestmentWithGrossPerformanceFromSellWithCurrencyEffect',
totalInvestmentWithGrossPerformanceFromSell.WithCurrencyEffect.toNumber()
);
console.log(
'grossPerformanceFromSells',
grossPerformanceFromSells.Value.toNumber()
);
console.log(
'grossPerformanceFromSellsWithCurrencyEffect',
grossPerformanceFromSells.WithCurrencyEffect.toNumber()
);
}
const newGrossPerformance = valueOfInvestment.Value.minus(
totalInvestment.Value
).plus(grossPerformanceFromSells.Value);
const newGrossPerformanceWithCurrencyEffect =
valueOfInvestment.WithCurrencyEffect.minus(
totalInvestment.WithCurrencyEffect
).plus(grossPerformanceFromSells.WithCurrencyEffect);
grossPerformance.Value = newGrossPerformance;
grossPerformance.WithCurrencyEffect =
newGrossPerformanceWithCurrencyEffect;
if (order.itemType === 'start') {
feesAtStartDate = {
Value: fees.Value,
WithCurrencyEffect: fees.WithCurrencyEffect
};
grossPerformanceAtStartDate.Value = grossPerformance.Value;
grossPerformanceAtStartDate.WithCurrencyEffect =
grossPerformance.WithCurrencyEffect;
}
totalInvestmentDays =
this.calculatePerformancesForDateAndReturnTotalInvestmentDays(
isChartMode,
i,
indexOfStartOrder,
currentValues,
order,
valueOfInvestment,
valueOfInvestmentBeforeTransaction,
netPerformanceValues,
grossPerformance,
grossPerformanceAtStartDate,
fees,
feesAtStartDate,
investmentValues,
investmentValuesAccumulated,
totalInvestment,
timeWeightedInvestmentValues,
previousOrderDateString,
totalInvestmentDays,
sumOfTimeWeightedInvestments,
valueAtStartDate,
investmentAtStartDate,
totalInvestmentBeforeTransaction,
transactionInvestment.WithCurrencyEffect
);
this.handleLoggingOfInvestmentMetrics(
totalInvestment,
order,
transactionInvestment,
totalInvestmentWithGrossPerformanceFromSell,
grossPerformanceFromSells,
grossPerformance,
grossPerformanceAtStartDate
);
if (i === indexOfEndOrder) {
break;
}
}
return {
lastAveragePrice,
grossPerformance,
feesAtStartDate,
grossPerformanceAtStartDate,
averagePriceAtStartDate,
totalUnits,
totalInvestment,
investmentAtStartDate,
valueAtStartDate,
maxTotalInvestment,
averagePriceAtEndDate,
initialValue,
fees,
netPerformanceValuesPercentage,
totalInvestmentDays
};
}
private handleFeeAndUnitPriceOfOrder(
order: PortfolioOrderItem,
currentExchangeRate: number,
exchangeRateAtOrderDate: number
) {
if (order.fee) {
order.feeInBaseCurrency = order.fee.mul(currentExchangeRate ?? 1);
order.feeInBaseCurrencyWithCurrencyEffect = order.fee.mul(
exchangeRateAtOrderDate ?? 1
);
}
if (order.unitPrice) {
order.unitPriceInBaseCurrency = order.unitPrice.mul(
currentExchangeRate ?? 1
);
order.unitPriceInBaseCurrencyWithCurrencyEffect = order.unitPrice.mul(
exchangeRateAtOrderDate ?? 1
);
}
}
private calculateNetPerformancePercentageForDateAndSymbol(
i: number,
orders: PortfolioOrderItem[],
order: PortfolioOrderItem,
netPerformanceValuesPercentage: { [date: string]: Big },
marketSymbolMap: { [date: string]: { [symbol: string]: Big } }
) {
if (i > 0 && order) {
const previousOrder = orders[i - 1];
if (order.unitPrice.toNumber() && previousOrder.unitPrice.toNumber()) {
netPerformanceValuesPercentage[order.date] = order.unitPrice
.div(previousOrder.unitPrice)
.minus(1);
} else if (
this.needsStakeHandling(order, marketSymbolMap, previousOrder)
) {
this.stakeHandling(
previousOrder,
marketSymbolMap,
netPerformanceValuesPercentage,
order
);
} else if (previousOrder.unitPrice.toNumber()) {
netPerformanceValuesPercentage[order.date] = new Big(-1);
} else if (
this.ispreviousOrderStakeAndHasInformation(
previousOrder,
marketSymbolMap
)
) {
this.handleIfPreviousOrderIsStake(
netPerformanceValuesPercentage,
order,
marketSymbolMap,
previousOrder
);
} else {
netPerformanceValuesPercentage[order.date] = new Big(0);
}
}
}
private handleIfPreviousOrderIsStake(
netPerformanceValuesPercentage: { [date: string]: Big },
order: PortfolioOrderItem,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
previousOrder: PortfolioOrderItem
) {
netPerformanceValuesPercentage[order.date] = order.unitPrice
.div(marketSymbolMap[previousOrder.date][previousOrder.symbol])
.minus(1);
}
private stakeHandling(
previousOrder: PortfolioOrderItem,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
netPerformanceValuesPercentage: { [date: string]: Big },
order: PortfolioOrderItem
) {
let previousUnitPrice =
previousOrder.type === 'STAKE'
? marketSymbolMap[previousOrder.date][previousOrder.symbol]
: previousOrder.unitPrice;
netPerformanceValuesPercentage[order.date] = marketSymbolMap[order.date][
order.symbol
]
? marketSymbolMap[order.date][order.symbol]
.div(previousUnitPrice)
.minus(1)
: new Big(0);
}
private ispreviousOrderStakeAndHasInformation(
previousOrder: PortfolioOrderItem,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } }
) {
return (
previousOrder.type === 'STAKE' &&
marketSymbolMap[previousOrder.date] &&
marketSymbolMap[previousOrder.date][previousOrder.symbol]?.toNumber()
);
}
private needsStakeHandling(
order: PortfolioOrderItem,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
previousOrder: PortfolioOrderItem
) {
return (
order.type === 'STAKE' &&
previousOrder &&
marketSymbolMap[order.date] &&
marketSymbolMap[previousOrder.date] &&
((marketSymbolMap[previousOrder.date][previousOrder.symbol]?.toNumber() &&
previousOrder.type === 'STAKE') ||
(previousOrder.type !== 'STAKE' && previousOrder.unitPrice?.toNumber()))
);
}
private handleLoggingOfInvestmentMetrics(
totalInvestment: WithCurrencyEffect<Big>,
order: PortfolioOrderItem,
transactionInvestment: WithCurrencyEffect<Big>,
totalInvestmentWithGrossPerformanceFromSell: WithCurrencyEffect<Big>,
grossPerformanceFromSells: WithCurrencyEffect<Big>,
grossPerformance: WithCurrencyEffect<Big>,
grossPerformanceAtStartDate: WithCurrencyEffect<Big>
) {
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log('totalInvestment', totalInvestment.Value.toNumber());
console.log('order.quantity', order.quantity.toNumber());
console.log(
'transactionInvestment',
transactionInvestment.Value.toNumber()
);
console.log(
'totalInvestmentWithGrossPerformanceFromSell',
totalInvestmentWithGrossPerformanceFromSell.Value.toNumber()
);
console.log(
'grossPerformanceFromSells',
grossPerformanceFromSells.Value.toNumber()
);
console.log('totalInvestment', totalInvestment.Value.toNumber());
console.log(
'totalGrossPerformance',
grossPerformance.Value.minus(
grossPerformanceAtStartDate.Value
).toNumber()
);
}
}
private calculateNetPerformancePercentage(
timeWeightedAverageInvestmentBetweenStartAndEndDate: WithCurrencyEffect<Big>,
totalNetPerformance: WithCurrencyEffect<Big>
) {
return {
Value: timeWeightedAverageInvestmentBetweenStartAndEndDate.Value.gt(0)
? totalNetPerformance.Value.div(
timeWeightedAverageInvestmentBetweenStartAndEndDate.Value
)
: new Big(0),
WithCurrencyEffect:
timeWeightedAverageInvestmentBetweenStartAndEndDate.WithCurrencyEffect.gt(
0
)
? totalNetPerformance.WithCurrencyEffect.div(
timeWeightedAverageInvestmentBetweenStartAndEndDate.WithCurrencyEffect
)
: new Big(0)
};
}
private calculateGrossPerformancePercentage(
averagePriceAtStartDate: Big,
averagePriceAtEndDate: Big,
orders: PortfolioOrderItem[],
indexOfStartOrder: number,
maxInvestmentBetweenStartAndEndDate: Big,
totalGrossPerformance: Big,
unitPriceAtEndDate: Big
) {
return PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT ||
averagePriceAtStartDate.eq(0) ||
averagePriceAtEndDate.eq(0) ||
orders[indexOfStartOrder].unitPrice.eq(0)
? maxInvestmentBetweenStartAndEndDate.gt(0)
? totalGrossPerformance.div(maxInvestmentBetweenStartAndEndDate)
: new Big(0)
: // This formula has the issue that buying more units with a price
// lower than the average buying price results in a positive
// performance even if the market price stays constant
unitPriceAtEndDate
.div(averagePriceAtEndDate)
.div(orders[indexOfStartOrder].unitPrice.div(averagePriceAtStartDate))
.minus(1);
}
private calculateInvestmentSpecificMetrics(
averagePriceAtStartDate: Big,
i: number,
indexOfStartOrder: number,
totalUnits: Big,
totalInvestment: WithCurrencyEffect<Big>,
order: PortfolioOrderItem,
investmentAtStartDate: WithCurrencyEffect<Big>,
valueAtStartDate: WithCurrencyEffect<Big>,
maxTotalInvestment: Big,
averagePriceAtEndDate: Big,
indexOfEndOrder: number,
initialValue: WithCurrencyEffect<Big>,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
fees: WithCurrencyEffect<Big>
) {
averagePriceAtStartDate = this.calculateAveragePrice(
averagePriceAtStartDate,
i,
indexOfStartOrder,
totalUnits,
totalInvestment.Value
);
const totalInvestmentBeforeTransaction = { ...totalInvestment };
const valueOfInvestmentBeforeTransaction = {
Value: totalUnits.mul(order.unitPrice),
WithCurrencyEffect: totalUnits.mul(
order.unitPriceInBaseCurrencyWithCurrencyEffect
)
};
if (
(investmentAtStartDate.Value?.eq(0) ?? true) &&
i >= indexOfStartOrder
) {
investmentAtStartDate.Value = totalInvestment.Value ?? new Big(0);
valueAtStartDate.Value = valueOfInvestmentBeforeTransaction.Value;
investmentAtStartDate.WithCurrencyEffect =
totalInvestment.WithCurrencyEffect ?? new Big(0);
valueAtStartDate.WithCurrencyEffect =
valueOfInvestmentBeforeTransaction.WithCurrencyEffect;
}
const transactionInvestment = {
Value: this.getTransactionInvestment(
order,
totalUnits,
totalInvestment.Value
),
WithCurrencyEffect: this.getTransactionInvestment(
order,
totalUnits,
totalInvestment.WithCurrencyEffect,
true
)
};
totalInvestment.Value = totalInvestment.Value.plus(
transactionInvestment.Value
);
totalInvestment.WithCurrencyEffect =
totalInvestment.WithCurrencyEffect.plus(
transactionInvestment.WithCurrencyEffect
);
if (
i >= indexOfStartOrder &&
totalInvestment.Value.gt(maxTotalInvestment)
) {
maxTotalInvestment = totalInvestment.Value;
}
averagePriceAtEndDate = this.calculateAveragePriceAtEnd(
i,
indexOfEndOrder,
totalUnits,
averagePriceAtEndDate,
totalInvestment.Value
);
initialValue = this.calculateInitialValue(
i,
indexOfStartOrder,
initialValue.Value,
valueOfInvestmentBeforeTransaction,
transactionInvestment,
order,
marketSymbolMap,
initialValue.WithCurrencyEffect
);
fees.Value = fees.Value.plus(order.fee);
fees.WithCurrencyEffect = fees.WithCurrencyEffect.plus(
order.feeInBaseCurrencyWithCurrencyEffect ?? 0
);
totalUnits = totalUnits.plus(
order.quantity.mul(this.getFactor(order.type))
);
const valueOfInvestment = {
Value: totalUnits.mul(order.unitPrice),
WithCurrencyEffect: totalUnits.mul(
order.unitPriceInBaseCurrencyWithCurrencyEffect
)
};
return {
transactionInvestment,
valueOfInvestment,
averagePriceAtStartDate,
totalUnits,
totalInvestment,
investmentAtStartDate,
valueAtStartDate,
maxTotalInvestment,
averagePriceAtEndDate,
initialValue,
fees,
totalInvestmentBeforeTransaction,
valueOfInvestmentBeforeTransaction
};
}
private calculatePerformancesForDateAndReturnTotalInvestmentDays(
isChartMode: boolean,
i: number,
indexOfStartOrder: number,
currentValues: WithCurrencyEffect<{ [date: string]: Big }>,
order: PortfolioOrderItem,
valueOfInvestment: WithCurrencyEffect<Big>,
valueOfInvestmentBeforeTransaction: WithCurrencyEffect<Big>,
netPerformanceValues: WithCurrencyEffect<{ [date: string]: Big }>,
grossPerformance: WithCurrencyEffect<Big>,
grossPerformanceAtStartDate: WithCurrencyEffect<Big>,
fees: WithCurrencyEffect<Big>,
feesAtStartDate: WithCurrencyEffect<Big>,
investmentValues: WithCurrencyEffect<{ [date: string]: Big }>,
investmentValuesAccumulated: WithCurrencyEffect<{ [date: string]: Big }>,
totalInvestment: WithCurrencyEffect<Big>,
timeWeightedInvestmentValues: WithCurrencyEffect<{ [date: string]: Big }>,
previousOrderDateString: string,
totalInvestmentDays: number,
sumOfTimeWeightedInvestments: WithCurrencyEffect<Big>,
valueAtStartDate: WithCurrencyEffect<Big>,
investmentAtStartDate: WithCurrencyEffect<Big>,
totalInvestmentBeforeTransaction: WithCurrencyEffect<Big>,
transactionInvestmentWithCurrencyEffect: Big
): number {
if (i > indexOfStartOrder) {
if (valueOfInvestmentBeforeTransaction.Value.gt(0)) {
// Calculate the number of days since the previous order
const orderDate = new Date(order.date);
const previousOrderDate = new Date(previousOrderDateString);
let daysSinceLastOrder = differenceInDays(orderDate, previousOrderDate);
// Set to at least 1 day, otherwise the transactions on the same day
// would not be considered in the time weighted calculation
if (daysSinceLastOrder <= 0) {
daysSinceLastOrder = 1;
}
// Sum up the total investment days since the start date to calculate
// the time weighted investment
totalInvestmentDays += daysSinceLastOrder;
sumOfTimeWeightedInvestments.Value =
sumOfTimeWeightedInvestments.Value.add(
valueAtStartDate.Value.minus(investmentAtStartDate.Value)
.plus(totalInvestmentBeforeTransaction.Value)
.mul(daysSinceLastOrder)
);
sumOfTimeWeightedInvestments.WithCurrencyEffect =
sumOfTimeWeightedInvestments.WithCurrencyEffect.add(
valueAtStartDate.WithCurrencyEffect.minus(
investmentAtStartDate.WithCurrencyEffect
)
.plus(totalInvestmentBeforeTransaction.WithCurrencyEffect)
.mul(daysSinceLastOrder)
);
}
if (isChartMode) {
currentValues.Value[order.date] = valueOfInvestment.Value;
currentValues.WithCurrencyEffect[order.date] =
valueOfInvestment.WithCurrencyEffect;
netPerformanceValues.Value[order.date] = grossPerformance.Value.minus(
grossPerformanceAtStartDate.Value
).minus(fees.Value.minus(feesAtStartDate.Value));
netPerformanceValues.WithCurrencyEffect[order.date] =
grossPerformance.WithCurrencyEffect.minus(
grossPerformanceAtStartDate.WithCurrencyEffect
).minus(
fees.WithCurrencyEffect.minus(feesAtStartDate.WithCurrencyEffect)
);
investmentValuesAccumulated.Value[order.date] = totalInvestment.Value;
investmentValuesAccumulated.WithCurrencyEffect[order.date] =
totalInvestment.WithCurrencyEffect;
investmentValues.WithCurrencyEffect[order.date] = (
investmentValues.WithCurrencyEffect[order.date] ?? new Big(0)
).add(transactionInvestmentWithCurrencyEffect);
timeWeightedInvestmentValues.Value[order.date] =
totalInvestmentDays > 0
? sumOfTimeWeightedInvestments.Value.div(totalInvestmentDays)
: new Big(0);
timeWeightedInvestmentValues.WithCurrencyEffect[order.date] =
totalInvestmentDays > 0
? sumOfTimeWeightedInvestments.WithCurrencyEffect.div(
totalInvestmentDays
)
: new Big(0);
}
}
return totalInvestmentDays;
}
private calculateSellOrders(
order: PortfolioOrderItem,
lastAveragePrice: WithCurrencyEffect<Big>,
grossPerformanceFromSells: WithCurrencyEffect<Big>,
totalInvestmentWithGrossPerformanceFromSell: WithCurrencyEffect<Big>,
transactionInvestment: WithCurrencyEffect<Big>
) {
const grossPerformanceFromSell =
order.type === TypeOfOrder.SELL
? order.unitPriceInBaseCurrency
.minus(lastAveragePrice.Value)
.mul(order.quantity)
: new Big(0);
const grossPerformanceFromSellWithCurrencyEffect =
order.type === TypeOfOrder.SELL
? order.unitPriceInBaseCurrencyWithCurrencyEffect
.minus(lastAveragePrice.WithCurrencyEffect)
.mul(order.quantity)
: new Big(0);
grossPerformanceFromSells.Value = grossPerformanceFromSells.Value.plus(
grossPerformanceFromSell
);
grossPerformanceFromSells.WithCurrencyEffect =
grossPerformanceFromSells.WithCurrencyEffect.plus(
grossPerformanceFromSellWithCurrencyEffect
);
totalInvestmentWithGrossPerformanceFromSell.Value =
totalInvestmentWithGrossPerformanceFromSell.Value.plus(
transactionInvestment.Value
).plus(grossPerformanceFromSell);
totalInvestmentWithGrossPerformanceFromSell.WithCurrencyEffect =
totalInvestmentWithGrossPerformanceFromSell.WithCurrencyEffect.plus(
transactionInvestment.WithCurrencyEffect
).plus(grossPerformanceFromSellWithCurrencyEffect);
return {
grossPerformanceFromSells,
totalInvestmentWithGrossPerformanceFromSell
};
}
private calculateInitialValue(
i: number,
indexOfStartOrder: number,
initialValue: Big,
valueOfInvestmentBeforeTransaction: WithCurrencyEffect<Big>,
transactionInvestment: WithCurrencyEffect<Big>,
order: PortfolioOrderItem,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
initialValueWithCurrencyEffect: Big
) {
if (i >= indexOfStartOrder && initialValue.gt(0)) {
if (
i === indexOfStartOrder &&
!valueOfInvestmentBeforeTransaction.Value.eq(0)
) {
initialValue = valueOfInvestmentBeforeTransaction.Value;
initialValueWithCurrencyEffect =
valueOfInvestmentBeforeTransaction.WithCurrencyEffect;
} else if (transactionInvestment.Value.gt(0)) {
initialValue = transactionInvestment.Value;
initialValueWithCurrencyEffect =
transactionInvestment.WithCurrencyEffect;
} else if (order.type === 'STAKE') {
// For Parachain Rewards or Stock SpinOffs, first transactionInvestment might be 0 if the symbol has been acquired for free
initialValue = order.quantity.mul(
marketSymbolMap[order.date]?.[order.symbol] ?? new Big(0)
);
}
}
return {
Value: initialValue,
WithCurrencyEffect: initialValueWithCurrencyEffect
};
}
private calculateAveragePriceAtEnd(
i: number,
indexOfEndOrder: number,
totalUnits: Big,
averagePriceAtEndDate: Big,
totalInvestment: Big
) {
if (i === indexOfEndOrder && totalUnits.gt(0)) {
averagePriceAtEndDate = totalInvestment.div(totalUnits);
}
return averagePriceAtEndDate;
}
private getTransactionInvestment(
order: PortfolioOrderItem,
totalUnits: Big,
totalInvestment: Big,
withCurrencyEffect: boolean = false
) {
return order.type === 'BUY' || order.type === 'STAKE'
? order.quantity
.mul(
withCurrencyEffect
? order.unitPriceInBaseCurrencyWithCurrencyEffect
: order.unitPrice
)
.mul(this.getFactor(order.type))
: totalUnits.gt(0)
? totalInvestment
.div(totalUnits)
.mul(order.quantity)
.mul(this.getFactor(order.type))
: new Big(0);
}
private calculateAveragePrice(
averagePriceAtStartDate: Big,
i: number,
indexOfStartOrder: number,
totalUnits: Big,
totalInvestment: Big
) {
if (
averagePriceAtStartDate.eq(0) &&
i >= indexOfStartOrder &&
totalUnits.gt(0)
) {
averagePriceAtStartDate = totalInvestment.div(totalUnits);
}
return averagePriceAtStartDate;
}
private handleStartOrder(
order: PortfolioOrderItem,
indexOfStartOrder: number,
orders: PortfolioOrderItem[],
i: number,
unitPriceAtStartDate: Big
) {
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
: unitPriceAtStartDate;
}
}
private handleLogging(
symbol: string,
orders: PortfolioOrderItem[],
indexOfStartOrder: number,
unitPriceAtEndDate: Big,
totalInvestment: WithCurrencyEffect<Big>,
totalGrossPerformance: WithCurrencyEffect<Big>,
grossPerformancePercentage: WithCurrencyEffect<Big>,
feesPerUnit: WithCurrencyEffect<Big>,
totalNetPerformance: WithCurrencyEffect<Big>,
netPerformancePercentage: WithCurrencyEffect<Big>
) {
if (PortfolioCalculator.ENABLE_LOGGING) {
console.log(
`
${symbol}
Unit price: ${orders[indexOfStartOrder].unitPrice.toFixed(
2
)} -> ${unitPriceAtEndDate.toFixed(2)}
Total investment: ${totalInvestment.Value.toFixed(2)}
Total investment with currency effect: ${totalInvestment.WithCurrencyEffect.toFixed(
2
)}
Gross performance: ${totalGrossPerformance.Value.toFixed(
2
)} / ${grossPerformancePercentage.Value.mul(100).toFixed(2)}%
Gross performance with currency effect: ${totalGrossPerformance.WithCurrencyEffect.toFixed(
2
)} / ${grossPerformancePercentage.WithCurrencyEffect.mul(100).toFixed(
2
)}%
Fees per unit: ${feesPerUnit.Value.toFixed(2)}
Fees per unit with currency effect: ${feesPerUnit.WithCurrencyEffect.toFixed(
2
)}
Net performance: ${totalNetPerformance.Value.toFixed(
2
)} / ${netPerformancePercentage.Value.mul(100).toFixed(2)}
Net performance with currency effect: ${totalNetPerformance.WithCurrencyEffect.toFixed(
2
)} / ${netPerformancePercentage.WithCurrencyEffect.mul(100).toFixed(2)}%`
);
}
}
private sortOrdersByTime(orders: PortfolioOrderItem[]) {
return sortBy(orders, (order) => {
let sortIndex = new Date(order.date);
if (order.itemType === 'start') {
sortIndex = addMilliseconds(sortIndex, -1);
}
if (order.itemType === 'end') {
sortIndex = addMilliseconds(sortIndex, 1);
}
return sortIndex.getTime();
});
}
private handleChartMode(
isChartMode: boolean,
orders: PortfolioOrderItem[],
day: Date,
end: Date,
symbol: string,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
lastUnitPrice: Big,
step: number
) {
if (isChartMode) {
const datesWithOrders = {};
for (const order of orders) {
datesWithOrders[order.date] = true;
}
while (isBefore(day, end)) {
this.handleDay(
datesWithOrders,
day,
orders,
symbol,
marketSymbolMap,
lastUnitPrice
);
lastUnitPrice = last(orders).unitPrice;
day = addDays(day, step);
}
}
return { day, lastUnitPrice };
}
private handleDay(
datesWithOrders: {},
day: Date,
orders: PortfolioOrderItem[],
symbol: string,
marketSymbolMap: { [date: string]: { [symbol: string]: Big } },
lastUnitPrice: Big
) {
const hasDate = datesWithOrders[format(day, DATE_FORMAT)];
if (!hasDate) {
orders.push({
symbol,
currency: null,
date: format(day, DATE_FORMAT),
dataSource: null,
fee: new Big(0),
name: '',
quantity: new Big(0),
type: TypeOfOrder.BUY,
unitPrice:
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? lastUnitPrice
});
} else {
let orderIndex = orders.findIndex(
(o) => o.date === format(day, DATE_FORMAT) && o.type === 'STAKE'
);
if (orderIndex >= 0) {
let order = orders[orderIndex];
orders.splice(orderIndex, 1);
orders.push({
...order,
unitPrice:
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? lastUnitPrice
});
}
}
}
private addSyntheticStartAndEndOrders(
orders: PortfolioOrderItem[],
symbol: string,
start: Date,
unitPriceAtStartDate: Big,
end: Date,
unitPriceAtEndDate: Big
) {
orders.push({
symbol,
currency: null,
date: format(start, DATE_FORMAT),
dataSource: null,
fee: new Big(0),
itemType: 'start',
name: '',
quantity: new Big(0),
type: TypeOfOrder.BUY,
unitPrice: unitPriceAtStartDate
});
orders.push({
symbol,
currency: null,
date: format(end, DATE_FORMAT),
dataSource: null,
fee: new Big(0),
itemType: 'end',
name: '',
quantity: new Big(0),
type: TypeOfOrder.BUY,
unitPrice: unitPriceAtEndDate
});
}
}