Browse Source

Respect step size when calculating snapshot (chart)

pull/3393/head
Reto Kaul 1 year ago
parent
commit
1dcceb1352
  1. 146
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  2. 30
      apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts

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

@ -37,12 +37,11 @@ import {
format, format,
isAfter, isAfter,
isBefore, isBefore,
isSameDay,
max, max,
min, min,
subDays subDays
} from 'date-fns'; } from 'date-fns';
import { first, last, sum, uniq, uniqBy } from 'lodash'; import { first, last, sortBy, sum, uniq, uniqBy } from 'lodash';
export abstract class PortfolioCalculator { export abstract class PortfolioCalculator {
protected static readonly ENABLE_LOGGING = false; protected static readonly ENABLE_LOGGING = false;
@ -195,15 +194,12 @@ export abstract class PortfolioCalculator {
const currencies: { [symbol: string]: string } = {}; const currencies: { [symbol: string]: string } = {};
const dataGatheringItems: IDataGatheringItem[] = []; const dataGatheringItems: IDataGatheringItem[] = [];
let dates: Date[] = [];
let firstIndex = transactionPoints.length; let firstIndex = transactionPoints.length;
let firstTransactionPoint: TransactionPoint = null; let firstTransactionPoint: TransactionPoint = null;
let totalInterestWithCurrencyEffect = new Big(0); let totalInterestWithCurrencyEffect = new Big(0);
let totalLiabilitiesWithCurrencyEffect = new Big(0); let totalLiabilitiesWithCurrencyEffect = new Big(0);
let totalValuablesWithCurrencyEffect = new Big(0); let totalValuablesWithCurrencyEffect = new Big(0);
dates.push(resetHours(start));
for (const { currency, dataSource, symbol } of transactionPoints[ for (const { currency, dataSource, symbol } of transactionPoints[
firstIndex - 1 firstIndex - 1
].items) { ].items) {
@ -223,36 +219,8 @@ export abstract class PortfolioCalculator {
firstTransactionPoint = transactionPoints[i]; firstTransactionPoint = transactionPoints[i];
firstIndex = i; firstIndex = i;
} }
if (firstTransactionPoint !== null) {
dates.push(resetHours(parseDate(transactionPoints[i].date)));
}
} }
dates.push(resetHours(endDate));
// 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 = let exchangeRatesByCurrency =
await this.exchangeRateDataService.getExchangeRatesByCurrency({ await this.exchangeRateDataService.getExchangeRatesByCurrency({
currencies: uniq(Object.values(currencies)), currencies: uniq(Object.values(currencies)),
@ -268,10 +236,7 @@ export abstract class PortfolioCalculator {
} = await this.currentRateService.getValues({ } = await this.currentRateService.getValues({
dataGatheringItems, dataGatheringItems,
dateQuery: { dateQuery: {
// TODO: Improve? gte: this.getStartDate(),
gte: firstTransactionPoint?.date
? parseDate(firstTransactionPoint.date)
: endDate,
lt: endDate lt: endDate
} }
}); });
@ -301,22 +266,15 @@ export abstract class PortfolioCalculator {
const chartStartDate = this.getStartDate(); const chartStartDate = this.getStartDate();
const daysInMarket = differenceInDays(endDate, chartStartDate) + 1; const daysInMarket = differenceInDays(endDate, chartStartDate) + 1;
const step = false /*withDataDecimation*/ let chartDateMap = this.getChartDateMap({
? Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)) endDate,
: 1; startDate: chartStartDate,
step: Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS))
let chartDates = eachDayOfInterval(
{ end: endDate, start: chartStartDate },
{ step }
).map((date) => {
return resetHours(date);
}); });
const includesEndDate = isSameDay(last(chartDates), endDate); const chartDates = sortBy(Object.keys(chartDateMap), (chartDate) => {
return chartDate;
if (!includesEndDate) { });
chartDates.push(resetHours(endDate));
}
if (firstIndex > 0) { if (firstIndex > 0) {
firstIndex--; firstIndex--;
@ -401,9 +359,9 @@ export abstract class PortfolioCalculator {
totalLiabilitiesInBaseCurrency, totalLiabilitiesInBaseCurrency,
totalValuablesInBaseCurrency totalValuablesInBaseCurrency
} = this.getSymbolMetrics({ } = this.getSymbolMetrics({
chartDateMap,
marketSymbolMap, marketSymbolMap,
start, start,
step,
dataSource: item.dataSource, dataSource: item.dataSource,
end: endDate, end: endDate,
exchangeRates: exchangeRates:
@ -500,9 +458,7 @@ export abstract class PortfolioCalculator {
} }
} }
for (const currentDate of chartDates) { for (const dateString of chartDates) {
const dateString = format(currentDate, DATE_FORMAT);
for (const symbol of Object.keys(valuesBySymbol)) { for (const symbol of Object.keys(valuesBySymbol)) {
const symbolValues = valuesBySymbol[symbol]; const symbolValues = valuesBySymbol[symbol];
@ -692,16 +648,6 @@ export abstract class PortfolioCalculator {
const dataGatheringItems: IDataGatheringItem[] = []; const dataGatheringItems: IDataGatheringItem[] = [];
const firstIndex = transactionPointsBeforeEndDate.length; const firstIndex = transactionPointsBeforeEndDate.length;
let dates = eachDayOfInterval({ start, end }, { step }).map((date) => {
return resetHours(date);
});
const includesEndDate = isSameDay(last(dates), end);
if (!includesEndDate) {
dates.push(resetHours(end));
}
if (transactionPointsBeforeEndDate.length > 0) { if (transactionPointsBeforeEndDate.length > 0) {
for (const { for (const {
currency, currency,
@ -781,6 +727,16 @@ export abstract class PortfolioCalculator {
}; };
} = {}; } = {};
let chartDateMap = this.getChartDateMap({
endDate: end,
startDate: start,
step
});
const chartDates = sortBy(Object.keys(chartDateMap), (chartDate) => {
return chartDate;
});
for (const symbol of Object.keys(symbols)) { for (const symbol of Object.keys(symbols)) {
const { const {
currentValues, currentValues,
@ -793,10 +749,10 @@ export abstract class PortfolioCalculator {
timeWeightedInvestmentValues, timeWeightedInvestmentValues,
timeWeightedInvestmentValuesWithCurrencyEffect timeWeightedInvestmentValuesWithCurrencyEffect
} = this.getSymbolMetrics({ } = this.getSymbolMetrics({
chartDateMap,
end, end,
marketSymbolMap, marketSymbolMap,
start, start,
step,
symbol, symbol,
dataSource: null, dataSource: null,
exchangeRates: exchangeRates:
@ -819,9 +775,7 @@ export abstract class PortfolioCalculator {
let lastDate = format(this.startDate, DATE_FORMAT); let lastDate = format(this.startDate, DATE_FORMAT);
for (const currentDate of dates) { for (const dateString of chartDates) {
const dateString = format(currentDate, DATE_FORMAT);
accumulatedValuesByDate[dateString] = { accumulatedValuesByDate[dateString] = {
investmentValueWithCurrencyEffect: new Big(0), investmentValueWithCurrencyEffect: new Big(0),
totalAccountBalanceWithCurrencyEffect: new Big(0), totalAccountBalanceWithCurrencyEffect: new Big(0),
@ -1181,15 +1135,16 @@ export abstract class PortfolioCalculator {
} }
protected abstract getSymbolMetrics({ protected abstract getSymbolMetrics({
chartDateMap,
dataSource, dataSource,
end, end,
exchangeRates, exchangeRates,
isChartMode, isChartMode,
marketSymbolMap, marketSymbolMap,
start, start,
step,
symbol symbol
}: { }: {
chartDateMap: { [date: string]: boolean };
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
isChartMode?: boolean; isChartMode?: boolean;
@ -1197,7 +1152,6 @@ export abstract class PortfolioCalculator {
[date: string]: { [symbol: string]: Big }; [date: string]: { [symbol: string]: Big };
}; };
start: Date; start: Date;
step?: number;
} & AssetProfileIdentifier): SymbolMetrics; } & AssetProfileIdentifier): SymbolMetrics;
public getTransactionPoints() { public getTransactionPoints() {
@ -1210,6 +1164,56 @@ export abstract class PortfolioCalculator {
return this.snapshot.totalValuablesWithCurrencyEffect; return this.snapshot.totalValuablesWithCurrencyEffect;
} }
private getChartDateMap({
endDate,
startDate,
step
}: {
endDate: Date;
startDate: Date;
step: number;
}) {
// Create a map of all relevant chart dates:
// 1. Add transaction point dates
let chartDateMap = this.transactionPoints.reduce((result, { date }) => {
result[date] = true;
return result;
}, {});
// 2. Add dates between transactions respecting the specified step size
for (let date of eachDayOfInterval(
{ end: endDate, start: startDate },
{ step }
)) {
chartDateMap[format(date, DATE_FORMAT)] = true;
}
// Make sure the end date is present
chartDateMap[format(endDate, DATE_FORMAT)] = true;
// Make sure some key dates are present
for (let dateRange of ['1d', '1y', '5y', 'max', 'mtd', 'wtd', 'ytd']) {
const { endDate: dateRangeEnd, startDate: dateRangeStart } =
getIntervalFromDateRange(dateRange);
if (
!isBefore(dateRangeStart, startDate) &&
!isAfter(dateRangeStart, endDate)
) {
chartDateMap[format(dateRangeStart, DATE_FORMAT)] = true;
}
if (
!isBefore(dateRangeEnd, startDate) &&
!isAfter(dateRangeEnd, endDate)
) {
chartDateMap[format(dateRangeEnd, DATE_FORMAT)] = true;
}
}
return chartDateMap;
}
private computeTransactionPoints() { private computeTransactionPoints() {
this.transactionPoints = []; this.transactionPoints = [];
const symbols: { [symbol: string]: TransactionPointSymbol } = {}; const symbols: { [symbol: string]: TransactionPointSymbol } = {};

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

@ -17,10 +17,8 @@ import {
addMilliseconds, addMilliseconds,
differenceInDays, differenceInDays,
eachDayOfInterval, eachDayOfInterval,
eachYearOfInterval,
format, format,
isBefore, isBefore
isThisYear
} from 'date-fns'; } from 'date-fns';
import { cloneDeep, first, last, sortBy } from 'lodash'; import { cloneDeep, first, last, sortBy } from 'lodash';
@ -143,15 +141,16 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
} }
protected getSymbolMetrics({ protected getSymbolMetrics({
chartDateMap,
dataSource, dataSource,
end, end,
exchangeRates, exchangeRates,
isChartMode = false, isChartMode = false,
marketSymbolMap, marketSymbolMap,
start, start,
step = 1,
symbol symbol
}: { }: {
chartDateMap?: { [date: string]: boolean };
end: Date; end: Date;
exchangeRates: { [dateString: string]: number }; exchangeRates: { [dateString: string]: number };
isChartMode?: boolean; isChartMode?: boolean;
@ -159,7 +158,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
[date: string]: { [symbol: string]: Big }; [date: string]: { [symbol: string]: Big };
}; };
start: Date; start: Date;
step?: number;
} & AssetProfileIdentifier): SymbolMetrics { } & AssetProfileIdentifier): SymbolMetrics {
const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)];
const currentValues: { [date: string]: Big } = {}; const currentValues: { [date: string]: Big } = {};
@ -352,15 +350,16 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
} }
while (isBefore(day, end)) { while (isBefore(day, end)) {
if (ordersByDate[format(day, DATE_FORMAT)]?.length > 0) { const dateString = format(day, DATE_FORMAT);
for (let order of ordersByDate[format(day, DATE_FORMAT)]) {
if (ordersByDate[dateString]?.length > 0) {
for (let order of ordersByDate[dateString]) {
order.unitPriceFromMarketData = order.unitPriceFromMarketData =
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice;
lastUnitPrice;
} }
} else { } else if (chartDateMap[dateString]) {
orders.push({ orders.push({
date: format(day, DATE_FORMAT), date: dateString,
fee: new Big(0), fee: new Big(0),
feeInBaseCurrency: new Big(0), feeInBaseCurrency: new Big(0),
quantity: new Big(0), quantity: new Big(0),
@ -369,12 +368,9 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
symbol symbol
}, },
type: 'BUY', type: 'BUY',
unitPrice: unitPrice: marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice,
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??
lastUnitPrice,
unitPriceFromMarketData: unitPriceFromMarketData:
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice
lastUnitPrice
}); });
} }
@ -383,7 +379,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
lastUnitPrice = lastUnitPrice =
lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice; lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice;
day = addDays(day, step); day = addDays(day, 1);
} }
} }

Loading…
Cancel
Save