|
|
|
@ -16,12 +16,11 @@ import {
|
|
|
|
|
isBefore,
|
|
|
|
|
isSameMonth,
|
|
|
|
|
isSameYear,
|
|
|
|
|
isWithinInterval,
|
|
|
|
|
max,
|
|
|
|
|
min,
|
|
|
|
|
set
|
|
|
|
|
} from 'date-fns';
|
|
|
|
|
import { first, flatten, isNumber, sortBy } from 'lodash';
|
|
|
|
|
import { first, flatten, isNumber, last, sortBy } from 'lodash';
|
|
|
|
|
|
|
|
|
|
import { CurrentRateService } from './current-rate.service';
|
|
|
|
|
import { CurrentPositions } from './interfaces/current-positions.interface';
|
|
|
|
@ -168,6 +167,131 @@ export class PortfolioCalculator {
|
|
|
|
|
this.transactionPoints = transactionPoints;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getChartData(start: Date, end = new Date(Date.now()), step = 1) {
|
|
|
|
|
const symbols: { [symbol: string]: boolean } = {};
|
|
|
|
|
|
|
|
|
|
const transactionPointsBeforeEndDate =
|
|
|
|
|
this.transactionPoints?.filter((transactionPoint) => {
|
|
|
|
|
return isBefore(parseDate(transactionPoint.date), end);
|
|
|
|
|
}) ?? [];
|
|
|
|
|
|
|
|
|
|
const firstIndex = transactionPointsBeforeEndDate.length;
|
|
|
|
|
const dates: Date[] = [];
|
|
|
|
|
const dataGatheringItems: IDataGatheringItem[] = [];
|
|
|
|
|
const currencies: { [symbol: string]: string } = {};
|
|
|
|
|
|
|
|
|
|
let day = start;
|
|
|
|
|
|
|
|
|
|
while (isBefore(day, end)) {
|
|
|
|
|
dates.push(resetHours(day));
|
|
|
|
|
day = addDays(day, step);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dates.push(resetHours(end));
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const marketSymbols = await this.currentRateService.getValues({
|
|
|
|
|
currencies,
|
|
|
|
|
dataGatheringItems,
|
|
|
|
|
dateQuery: {
|
|
|
|
|
in: dates
|
|
|
|
|
},
|
|
|
|
|
userCurrency: this.currency
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const 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.marketPriceInBaseCurrency) {
|
|
|
|
|
marketSymbolMap[dateString][marketSymbol.symbol] = new Big(
|
|
|
|
|
marketSymbol.marketPriceInBaseCurrency
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const netPerformanceValuesBySymbol: {
|
|
|
|
|
[symbol: string]: { [date: string]: Big };
|
|
|
|
|
} = {};
|
|
|
|
|
|
|
|
|
|
const investmentValuesBySymbol: {
|
|
|
|
|
[symbol: string]: { [date: string]: Big };
|
|
|
|
|
} = {};
|
|
|
|
|
|
|
|
|
|
const totalNetPerformanceValues: { [date: string]: Big } = {};
|
|
|
|
|
const totalInvestmentValues: { [date: string]: Big } = {};
|
|
|
|
|
|
|
|
|
|
for (const symbol of Object.keys(symbols)) {
|
|
|
|
|
const { netPerformanceValues, investmentValues } = this.getSymbolMetrics({
|
|
|
|
|
end,
|
|
|
|
|
marketSymbolMap,
|
|
|
|
|
start,
|
|
|
|
|
step,
|
|
|
|
|
symbol,
|
|
|
|
|
isChartMode: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
netPerformanceValuesBySymbol[symbol] = netPerformanceValues;
|
|
|
|
|
investmentValuesBySymbol[symbol] = investmentValues;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const currentDate of dates) {
|
|
|
|
|
const dateString = format(currentDate, DATE_FORMAT);
|
|
|
|
|
|
|
|
|
|
for (const symbol of Object.keys(netPerformanceValuesBySymbol)) {
|
|
|
|
|
totalNetPerformanceValues[dateString] =
|
|
|
|
|
totalNetPerformanceValues[dateString] ?? new Big(0);
|
|
|
|
|
|
|
|
|
|
if (netPerformanceValuesBySymbol[symbol]?.[dateString]) {
|
|
|
|
|
totalNetPerformanceValues[dateString] = totalNetPerformanceValues[
|
|
|
|
|
dateString
|
|
|
|
|
].add(netPerformanceValuesBySymbol[symbol][dateString]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
totalInvestmentValues[dateString] =
|
|
|
|
|
totalInvestmentValues[dateString] ?? new Big(0);
|
|
|
|
|
|
|
|
|
|
if (investmentValuesBySymbol[symbol]?.[dateString]) {
|
|
|
|
|
totalInvestmentValues[dateString] = totalInvestmentValues[
|
|
|
|
|
dateString
|
|
|
|
|
].add(investmentValuesBySymbol[symbol][dateString]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isInPercentage = true;
|
|
|
|
|
|
|
|
|
|
return Object.keys(totalNetPerformanceValues).map((date) => {
|
|
|
|
|
return isInPercentage
|
|
|
|
|
? {
|
|
|
|
|
date,
|
|
|
|
|
value: totalInvestmentValues[date].eq(0)
|
|
|
|
|
? 0
|
|
|
|
|
: totalNetPerformanceValues[date]
|
|
|
|
|
.div(totalInvestmentValues[date])
|
|
|
|
|
.mul(100)
|
|
|
|
|
.toNumber()
|
|
|
|
|
}
|
|
|
|
|
: {
|
|
|
|
|
date,
|
|
|
|
|
value: totalNetPerformanceValues[date].toNumber()
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async getCurrentPositions(
|
|
|
|
|
start: Date,
|
|
|
|
|
end = new Date(Date.now())
|
|
|
|
@ -710,15 +834,19 @@ export class PortfolioCalculator {
|
|
|
|
|
|
|
|
|
|
private getSymbolMetrics({
|
|
|
|
|
end,
|
|
|
|
|
isChartMode = false,
|
|
|
|
|
marketSymbolMap,
|
|
|
|
|
start,
|
|
|
|
|
step = 1,
|
|
|
|
|
symbol
|
|
|
|
|
}: {
|
|
|
|
|
end: Date;
|
|
|
|
|
isChartMode?: boolean;
|
|
|
|
|
marketSymbolMap: {
|
|
|
|
|
[date: string]: { [symbol: string]: Big };
|
|
|
|
|
};
|
|
|
|
|
start: Date;
|
|
|
|
|
step?: number;
|
|
|
|
|
symbol: string;
|
|
|
|
|
}) {
|
|
|
|
|
let orders: PortfolioOrderItem[] = this.orders.filter((order) => {
|
|
|
|
@ -767,10 +895,12 @@ export class PortfolioCalculator {
|
|
|
|
|
let grossPerformanceFromSells = new Big(0);
|
|
|
|
|
let initialValue: Big;
|
|
|
|
|
let investmentAtStartDate: Big;
|
|
|
|
|
const investmentValues: { [date: string]: Big } = {};
|
|
|
|
|
let lastAveragePrice = new Big(0);
|
|
|
|
|
let lastTransactionInvestment = new Big(0);
|
|
|
|
|
let lastValueOfInvestmentBeforeTransaction = new Big(0);
|
|
|
|
|
let maxTotalInvestment = new Big(0);
|
|
|
|
|
const netPerformanceValues: { [date: string]: Big } = {};
|
|
|
|
|
let timeWeightedGrossPerformancePercentage = new Big(1);
|
|
|
|
|
let timeWeightedNetPerformancePercentage = new Big(1);
|
|
|
|
|
let totalInvestment = new Big(0);
|
|
|
|
@ -805,6 +935,41 @@ export class PortfolioCalculator {
|
|
|
|
|
unitPrice: unitPriceAtEndDate
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let day = start;
|
|
|
|
|
let lastUnitPrice: Big;
|
|
|
|
|
|
|
|
|
|
if (isChartMode) {
|
|
|
|
|
const datesWithOrders = {};
|
|
|
|
|
|
|
|
|
|
for (const order of orders) {
|
|
|
|
|
datesWithOrders[order.date] = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (isBefore(day, end)) {
|
|
|
|
|
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
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastUnitPrice = last(orders).unitPrice;
|
|
|
|
|
|
|
|
|
|
day = addDays(day, step);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort orders so that the start and end placeholder order are at the right
|
|
|
|
|
// position
|
|
|
|
|
orders = sortBy(orders, (order) => {
|
|
|
|
@ -968,6 +1133,14 @@ export class PortfolioCalculator {
|
|
|
|
|
grossPerformanceAtStartDate = grossPerformance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isChartMode && i > indexOfStartOrder) {
|
|
|
|
|
netPerformanceValues[order.date] = grossPerformance
|
|
|
|
|
.minus(grossPerformanceAtStartDate)
|
|
|
|
|
.minus(fees.minus(feesAtStartDate));
|
|
|
|
|
|
|
|
|
|
investmentValues[order.date] = totalInvestment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (i === indexOfEndOrder) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
@ -1056,7 +1229,9 @@ export class PortfolioCalculator {
|
|
|
|
|
return {
|
|
|
|
|
initialValue,
|
|
|
|
|
grossPerformancePercentage,
|
|
|
|
|
investmentValues,
|
|
|
|
|
netPerformancePercentage,
|
|
|
|
|
netPerformanceValues,
|
|
|
|
|
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
|
|
|
|
|
netPerformance: totalNetPerformance,
|
|
|
|
|
grossPerformance: totalGrossPerformance
|
|
|
|
|