From c9cd59f443123a4cdfaea3939c1a5a07434abc1d Mon Sep 17 00:00:00 2001 From: ceroma <678940+ceroma@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:54:18 -0300 Subject: [PATCH] Feature/improve performance of portfolio snapshot computation - 2 Continuing my perf analysis of the portfolio snapshot computation, this was another big time sink on my machine, specially when generating each day for the `'5y'` and `'max'` date ranges. For reference, before this diff, the `dateRange` loop was taking on average ~100ms per symbol for me (Raspberry Pi 4, 478 symbols in the DB). After this diff, it takes ~15ms per symbol, shaving off 40s of computation time. Test Plan: Check that result of portfolio snapshot computation is the same before and after this diff: 1. Flush portfolio snapshot cache: ``` $ docker exec -it ghostfolio-redis-1 redis-cli --pass $REDIS_PASSWORD del "portfolio-snapshot-f9e4c63e-4b8e-46fc-b6ed-75ca0205f12b" ``` 2. Open Accounts to trigger snapshot calculation 3. Dump portfolio snapshot to a file: ``` $ docker exec -it ghostfolio-redis-1 redis-cli --pass $REDIS_PASSWORD --no-auth-warning get "portfolio-snapshot-f9e4c63e-4b8e-46fc-b6ed-75ca0205f12b" | python -c "import json, sys; json.dump(json.loads(json.loads(json.load(sys.stdin))), sys.stdout, indent=2, sort_keys=True)" > snapshot-before.json ``` 4. Apply this patch 5. Repeat steps 1-3 6. Compare results, check everything matches except for expiration time: ``` $ diff snapshot-before.json snapshot-after.json 2c2 < "expiration": 1727449402720, --- > "expiration": 1727449639794, ``` --- .../calculator/twr/portfolio-calculator.ts | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) 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 3b64cd185..6ce361eb9 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -16,13 +16,14 @@ import { addDays, addMilliseconds, differenceInDays, - eachDayOfInterval, format, isBefore } from 'date-fns'; import { cloneDeep, first, last, sortBy } from 'lodash'; export class TWRPortfolioCalculator extends PortfolioCalculator { + private chartDatesDescending: string[]; + protected calculateOverallPerformance( positions: TimelinePosition[] ): PortfolioSnapshot { @@ -820,31 +821,35 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { startDate = start; } + const startDateString = format(startDate, DATE_FORMAT); + const endDateString = format(endDate, DATE_FORMAT); + const currentValuesAtDateRangeStartWithCurrencyEffect = - currentValuesWithCurrencyEffect[format(startDate, DATE_FORMAT)] ?? - new Big(0); + currentValuesWithCurrencyEffect[startDateString] ?? new Big(0); const investmentValuesAccumulatedAtStartDateWithCurrencyEffect = - investmentValuesAccumulatedWithCurrencyEffect[ - format(startDate, DATE_FORMAT) - ] ?? new Big(0); + investmentValuesAccumulatedWithCurrencyEffect[startDateString] ?? + new Big(0); const grossPerformanceAtDateRangeStartWithCurrencyEffect = currentValuesAtDateRangeStartWithCurrencyEffect.minus( investmentValuesAccumulatedAtStartDateWithCurrencyEffect ); - const dates = eachDayOfInterval({ - end: endDate, - start: startDate - }).map((date) => { - return format(date, DATE_FORMAT); - }); - let average = new Big(0); let dayCount = 0; - for (const date of dates) { + if (!this.chartDatesDescending) { + this.chartDatesDescending = Object.keys(chartDateMap).sort().reverse(); + } + + for (const date of this.chartDatesDescending) { + if (date > endDateString) { + continue; + } else if (date < startDateString) { + break; + } + if ( investmentValuesAccumulatedWithCurrencyEffect[date] instanceof Big && investmentValuesAccumulatedWithCurrencyEffect[date].gt(0) @@ -864,17 +869,14 @@ export class TWRPortfolioCalculator extends PortfolioCalculator { } netPerformanceWithCurrencyEffectMap[dateRange] = - netPerformanceValuesWithCurrencyEffect[ - format(endDate, DATE_FORMAT) - ]?.minus( + netPerformanceValuesWithCurrencyEffect[endDateString]?.minus( // If the date range is 'max', take 0 as a start value. Otherwise, // the value of the end of the day of the start date is taken which // differs from the buying price. dateRange === 'max' ? new Big(0) - : (netPerformanceValuesWithCurrencyEffect[ - format(startDate, DATE_FORMAT) - ] ?? new Big(0)) + : (netPerformanceValuesWithCurrencyEffect[startDateString] ?? + new Big(0)) ) ?? new Big(0); netPerformancePercentageWithCurrencyEffectMap[dateRange] = average.gt(0)