From b2ed0b2c805ce06de482cd8a12eb6abf1ad9faea Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 5 Aug 2024 19:44:24 +0200 Subject: [PATCH] Feature/improve caching of benchmarks in markets overview (#3640) * Improve caching * Update changelog --- CHANGELOG.md | 1 + .../src/app/benchmark/benchmark.service.ts | 190 ++++++++++-------- .../interfaces/benchmark-value.interface.ts | 6 + 3 files changed, 112 insertions(+), 85 deletions(-) create mode 100644 apps/api/src/app/benchmark/interfaces/benchmark-value.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 02b33340b..b16284ab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Improved the caching of the benchmarks in the markets overview by returning cached data and recalculating in the background when it expires - Improved the language localization for German (`de`) - Improved the language localization for Polish (`pl`) - Upgraded `Nx` from version `19.5.1` to `19.5.6` diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts index e9495b44b..b6fdc8ea9 100644 --- a/apps/api/src/app/benchmark/benchmark.service.ts +++ b/apps/api/src/app/benchmark/benchmark.service.ts @@ -29,15 +29,19 @@ import { Injectable, Logger } from '@nestjs/common'; import { SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; import { + addHours, differenceInDays, eachDayOfInterval, format, + isAfter, isSameDay, subDays } from 'date-fns'; import { isNumber, last, uniqBy } from 'lodash'; import ms from 'ms'; +import { BenchmarkValue } from './interfaces/benchmark-value.interface'; + @Injectable() export class BenchmarkService { private readonly CACHE_KEY_BENCHMARKS = 'BENCHMARKS'; @@ -92,99 +96,26 @@ export class BenchmarkService { enableSharing = false, useCache = true } = {}): Promise { - let benchmarks: BenchmarkResponse['benchmarks']; - if (useCache) { try { - benchmarks = JSON.parse( - await this.redisCacheService.get(this.CACHE_KEY_BENCHMARKS) + const cachedBenchmarkValue = await this.redisCacheService.get( + this.CACHE_KEY_BENCHMARKS ); - if (benchmarks) { - return benchmarks; - } - } catch {} - } - - const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles({ - enableSharing - }); - - const promisesAllTimeHighs: Promise<{ date: Date; marketPrice: number }>[] = - []; - const promisesBenchmarkTrends: Promise<{ - trend50d: BenchmarkTrend; - trend200d: BenchmarkTrend; - }>[] = []; - - const quotes = await this.dataProviderService.getQuotes({ - items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => { - return { dataSource, symbol }; - }), - requestTimeout: ms('30 seconds'), - useCache: false - }); - - for (const { dataSource, symbol } of benchmarkAssetProfiles) { - promisesAllTimeHighs.push( - this.marketDataService.getMax({ dataSource, symbol }) - ); - promisesBenchmarkTrends.push( - this.getBenchmarkTrends({ dataSource, symbol }) - ); - } - - const [allTimeHighs, benchmarkTrends] = await Promise.all([ - Promise.all(promisesAllTimeHighs), - Promise.all(promisesBenchmarkTrends) - ]); - let storeInCache = useCache; + const { benchmarks, expiration }: BenchmarkValue = + JSON.parse(cachedBenchmarkValue); - benchmarks = allTimeHighs.map((allTimeHigh, index) => { - const { marketPrice } = - quotes[benchmarkAssetProfiles[index].symbol] ?? {}; - - let performancePercentFromAllTimeHigh = 0; - - if (allTimeHigh?.marketPrice && marketPrice) { - performancePercentFromAllTimeHigh = this.calculateChangeInPercentage( - allTimeHigh.marketPrice, - marketPrice - ); - } else { - storeInCache = false; - } - - return { - dataSource: benchmarkAssetProfiles[index].dataSource, - marketCondition: this.getMarketCondition( - performancePercentFromAllTimeHigh - ), - name: benchmarkAssetProfiles[index].name, - performances: { - allTimeHigh: { - date: allTimeHigh?.date, - performancePercent: - performancePercentFromAllTimeHigh >= 0 - ? 0 - : performancePercentFromAllTimeHigh - } - }, - symbol: benchmarkAssetProfiles[index].symbol, - trend50d: benchmarkTrends[index].trend50d, - trend200d: benchmarkTrends[index].trend200d - }; - }); + if (isAfter(new Date(), new Date(expiration))) { + this.calculateAndCacheBenchmarks({ + enableSharing + }); + } - if (storeInCache) { - await this.redisCacheService.set( - this.CACHE_KEY_BENCHMARKS, - JSON.stringify(benchmarks), - ms('2 hours') / 1000 - ); + return benchmarks; + } catch {} } - return benchmarks; + return this.calculateAndCacheBenchmarks({ enableSharing }); } public async getBenchmarkAssetProfiles({ @@ -422,6 +353,95 @@ export class BenchmarkService { }; } + private async calculateAndCacheBenchmarks({ + enableSharing = false + }): Promise { + const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles({ + enableSharing + }); + + const promisesAllTimeHighs: Promise<{ date: Date; marketPrice: number }>[] = + []; + const promisesBenchmarkTrends: Promise<{ + trend50d: BenchmarkTrend; + trend200d: BenchmarkTrend; + }>[] = []; + + const quotes = await this.dataProviderService.getQuotes({ + items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => { + return { dataSource, symbol }; + }), + requestTimeout: ms('30 seconds'), + useCache: false + }); + + for (const { dataSource, symbol } of benchmarkAssetProfiles) { + promisesAllTimeHighs.push( + this.marketDataService.getMax({ dataSource, symbol }) + ); + promisesBenchmarkTrends.push( + this.getBenchmarkTrends({ dataSource, symbol }) + ); + } + + const [allTimeHighs, benchmarkTrends] = await Promise.all([ + Promise.all(promisesAllTimeHighs), + Promise.all(promisesBenchmarkTrends) + ]); + let storeInCache = true; + + const benchmarks = allTimeHighs.map((allTimeHigh, index) => { + const { marketPrice } = + quotes[benchmarkAssetProfiles[index].symbol] ?? {}; + + let performancePercentFromAllTimeHigh = 0; + + if (allTimeHigh?.marketPrice && marketPrice) { + performancePercentFromAllTimeHigh = this.calculateChangeInPercentage( + allTimeHigh.marketPrice, + marketPrice + ); + } else { + storeInCache = false; + } + + return { + dataSource: benchmarkAssetProfiles[index].dataSource, + marketCondition: this.getMarketCondition( + performancePercentFromAllTimeHigh + ), + name: benchmarkAssetProfiles[index].name, + performances: { + allTimeHigh: { + date: allTimeHigh?.date, + performancePercent: + performancePercentFromAllTimeHigh >= 0 + ? 0 + : performancePercentFromAllTimeHigh + } + }, + symbol: benchmarkAssetProfiles[index].symbol, + trend50d: benchmarkTrends[index].trend50d, + trend200d: benchmarkTrends[index].trend200d + }; + }); + + if (storeInCache) { + const expiration = addHours(new Date(), 2); + + await this.redisCacheService.set( + this.CACHE_KEY_BENCHMARKS, + JSON.stringify({ + benchmarks, + expiration: expiration.getTime() + }), + ms('12 hours') / 1000 + ); + } + + return benchmarks; + } + private getMarketCondition( aPerformanceInPercent: number ): Benchmark['marketCondition'] { diff --git a/apps/api/src/app/benchmark/interfaces/benchmark-value.interface.ts b/apps/api/src/app/benchmark/interfaces/benchmark-value.interface.ts new file mode 100644 index 000000000..eda302f90 --- /dev/null +++ b/apps/api/src/app/benchmark/interfaces/benchmark-value.interface.ts @@ -0,0 +1,6 @@ +import { BenchmarkResponse } from '@ghostfolio/common/interfaces'; + +export interface BenchmarkValue { + benchmarks: BenchmarkResponse['benchmarks']; + expiration: number; +}