From 1dcceb1352c1fa0907547496ae9c375b14dabd32 Mon Sep 17 00:00:00 2001 From: Reto Kaul Date: Sat, 17 Aug 2024 16:24:33 +0200 Subject: [PATCH 1/2] Respect step size when calculating snapshot (chart) --- .../calculator/portfolio-calculator.ts | 146 +++++++++--------- .../calculator/twr/portfolio-calculator.ts | 30 ++-- 2 files changed, 88 insertions(+), 88 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index b5ec98b56..06e499de2 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -37,12 +37,11 @@ import { format, isAfter, isBefore, - isSameDay, max, min, subDays } from 'date-fns'; -import { first, last, sum, uniq, uniqBy } from 'lodash'; +import { first, last, sortBy, sum, uniq, uniqBy } from 'lodash'; export abstract class PortfolioCalculator { protected static readonly ENABLE_LOGGING = false; @@ -195,15 +194,12 @@ export abstract class PortfolioCalculator { const currencies: { [symbol: string]: string } = {}; const dataGatheringItems: IDataGatheringItem[] = []; - let dates: Date[] = []; let firstIndex = transactionPoints.length; let firstTransactionPoint: TransactionPoint = null; let totalInterestWithCurrencyEffect = new Big(0); let totalLiabilitiesWithCurrencyEffect = new Big(0); let totalValuablesWithCurrencyEffect = new Big(0); - dates.push(resetHours(start)); - for (const { currency, dataSource, symbol } of transactionPoints[ firstIndex - 1 ].items) { @@ -223,36 +219,8 @@ export abstract class PortfolioCalculator { firstTransactionPoint = transactionPoints[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 = await this.exchangeRateDataService.getExchangeRatesByCurrency({ currencies: uniq(Object.values(currencies)), @@ -268,10 +236,7 @@ export abstract class PortfolioCalculator { } = await this.currentRateService.getValues({ dataGatheringItems, dateQuery: { - // TODO: Improve? - gte: firstTransactionPoint?.date - ? parseDate(firstTransactionPoint.date) - : endDate, + gte: this.getStartDate(), lt: endDate } }); @@ -301,22 +266,15 @@ export abstract class PortfolioCalculator { const chartStartDate = this.getStartDate(); const daysInMarket = differenceInDays(endDate, chartStartDate) + 1; - const step = false /*withDataDecimation*/ - ? Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)) - : 1; - - let chartDates = eachDayOfInterval( - { end: endDate, start: chartStartDate }, - { step } - ).map((date) => { - return resetHours(date); + let chartDateMap = this.getChartDateMap({ + endDate, + startDate: chartStartDate, + step: Math.round(daysInMarket / Math.min(daysInMarket, MAX_CHART_ITEMS)) }); - const includesEndDate = isSameDay(last(chartDates), endDate); - - if (!includesEndDate) { - chartDates.push(resetHours(endDate)); - } + const chartDates = sortBy(Object.keys(chartDateMap), (chartDate) => { + return chartDate; + }); if (firstIndex > 0) { firstIndex--; @@ -401,9 +359,9 @@ export abstract class PortfolioCalculator { totalLiabilitiesInBaseCurrency, totalValuablesInBaseCurrency } = this.getSymbolMetrics({ + chartDateMap, marketSymbolMap, start, - step, dataSource: item.dataSource, end: endDate, exchangeRates: @@ -500,9 +458,7 @@ export abstract class PortfolioCalculator { } } - for (const currentDate of chartDates) { - const dateString = format(currentDate, DATE_FORMAT); - + for (const dateString of chartDates) { for (const symbol of Object.keys(valuesBySymbol)) { const symbolValues = valuesBySymbol[symbol]; @@ -692,16 +648,6 @@ export abstract class PortfolioCalculator { const dataGatheringItems: IDataGatheringItem[] = []; 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) { for (const { 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)) { const { currentValues, @@ -793,10 +749,10 @@ export abstract class PortfolioCalculator { timeWeightedInvestmentValues, timeWeightedInvestmentValuesWithCurrencyEffect } = this.getSymbolMetrics({ + chartDateMap, end, marketSymbolMap, start, - step, symbol, dataSource: null, exchangeRates: @@ -819,9 +775,7 @@ export abstract class PortfolioCalculator { let lastDate = format(this.startDate, DATE_FORMAT); - for (const currentDate of dates) { - const dateString = format(currentDate, DATE_FORMAT); - + for (const dateString of chartDates) { accumulatedValuesByDate[dateString] = { investmentValueWithCurrencyEffect: new Big(0), totalAccountBalanceWithCurrencyEffect: new Big(0), @@ -1181,15 +1135,16 @@ export abstract class PortfolioCalculator { } protected abstract getSymbolMetrics({ + chartDateMap, dataSource, end, exchangeRates, isChartMode, marketSymbolMap, start, - step, symbol }: { + chartDateMap: { [date: string]: boolean }; end: Date; exchangeRates: { [dateString: string]: number }; isChartMode?: boolean; @@ -1197,7 +1152,6 @@ export abstract class PortfolioCalculator { [date: string]: { [symbol: string]: Big }; }; start: Date; - step?: number; } & AssetProfileIdentifier): SymbolMetrics; public getTransactionPoints() { @@ -1210,6 +1164,56 @@ export abstract class PortfolioCalculator { 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() { this.transactionPoints = []; const symbols: { [symbol: string]: TransactionPointSymbol } = {}; diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index 3b0f755fd..b5861004a 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -17,10 +17,8 @@ import { addMilliseconds, differenceInDays, eachDayOfInterval, - eachYearOfInterval, format, - isBefore, - isThisYear + isBefore } from 'date-fns'; import { cloneDeep, first, last, sortBy } from 'lodash'; @@ -143,15 +141,16 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { } protected getSymbolMetrics({ + chartDateMap, dataSource, end, exchangeRates, isChartMode = false, marketSymbolMap, start, - step = 1, symbol }: { + chartDateMap?: { [date: string]: boolean }; end: Date; exchangeRates: { [dateString: string]: number }; isChartMode?: boolean; @@ -159,7 +158,6 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { [date: string]: { [symbol: string]: Big }; }; start: Date; - step?: number; } & AssetProfileIdentifier): SymbolMetrics { const currentExchangeRate = exchangeRates[format(new Date(), DATE_FORMAT)]; const currentValues: { [date: string]: Big } = {}; @@ -352,15 +350,16 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { } while (isBefore(day, end)) { - if (ordersByDate[format(day, DATE_FORMAT)]?.length > 0) { - for (let order of ordersByDate[format(day, DATE_FORMAT)]) { + const dateString = format(day, DATE_FORMAT); + + if (ordersByDate[dateString]?.length > 0) { + for (let order of ordersByDate[dateString]) { order.unitPriceFromMarketData = - marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? - lastUnitPrice; + marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice; } - } else { + } else if (chartDateMap[dateString]) { orders.push({ - date: format(day, DATE_FORMAT), + date: dateString, fee: new Big(0), feeInBaseCurrency: new Big(0), quantity: new Big(0), @@ -369,12 +368,9 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { symbol }, type: 'BUY', - unitPrice: - marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? - lastUnitPrice, + unitPrice: marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice, unitPriceFromMarketData: - marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ?? - lastUnitPrice + marketSymbolMap[dateString]?.[symbol] ?? lastUnitPrice }); } @@ -383,7 +379,7 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice; - day = addDays(day, step); + day = addDays(day, 1); } } From d92fc8cf1a2011a07d30d234429b54382eb48da5 Mon Sep 17 00:00:00 2001 From: Reto Kaul Date: Sat, 17 Aug 2024 17:03:49 +0200 Subject: [PATCH 2/2] Improvement after code review --- apps/api/src/app/portfolio/calculator/portfolio-calculator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 06e499de2..812b14d6e 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -728,9 +728,9 @@ export abstract class PortfolioCalculator { } = {}; let chartDateMap = this.getChartDateMap({ + step, endDate: end, - startDate: start, - step + startDate: start }); const chartDates = sortBy(Object.keys(chartDateMap), (chartDate) => {