diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts index 1717fa6b5..b531ffc9d 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts @@ -3,7 +3,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; +import { Filter, HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; @@ -30,27 +30,23 @@ export class PortfolioCalculatorFactory { activities, calculationType, currency, - hasFilters, - isExperimentalFeatures = false, + filters = [], userId }: { accountBalanceItems?: HistoricalDataItem[]; activities: Activity[]; calculationType: PerformanceCalculationType; currency: string; - hasFilters: boolean; - isExperimentalFeatures?: boolean; + filters?: Filter[]; userId: string; }): PortfolioCalculator { - const useCache = true; // TODO - switch (calculationType) { case PerformanceCalculationType.MWR: return new MWRPortfolioCalculator({ accountBalanceItems, activities, currency, - useCache, + filters, userId, configurationService: this.configurationService, currentRateService: this.currentRateService, @@ -63,7 +59,7 @@ export class PortfolioCalculatorFactory { activities, currency, currentRateService: this.currentRateService, - useCache, + filters, userId, configurationService: this.configurationService, exchangeRateDataService: this.exchangeRateDataService, diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index fe389ba19..7eef8b469 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -19,6 +19,7 @@ import { import { AssetProfileIdentifier, DataProviderInfo, + Filter, HistoricalDataItem, InvestmentItem, ResponseError, @@ -54,12 +55,12 @@ export abstract class PortfolioCalculator { private dataProviderInfos: DataProviderInfo[]; private endDate: Date; private exchangeRateDataService: ExchangeRateDataService; + private filters: Filter[]; private redisCacheService: RedisCacheService; private snapshot: PortfolioSnapshot; private snapshotPromise: Promise; private startDate: Date; private transactionPoints: TransactionPoint[]; - private useCache: boolean; private userId: string; public constructor({ @@ -69,8 +70,8 @@ export abstract class PortfolioCalculator { currency, currentRateService, exchangeRateDataService, + filters, redisCacheService, - useCache, userId }: { accountBalanceItems: HistoricalDataItem[]; @@ -79,8 +80,8 @@ export abstract class PortfolioCalculator { currency: string; currentRateService: CurrentRateService; exchangeRateDataService: ExchangeRateDataService; + filters: Filter[]; redisCacheService: RedisCacheService; - useCache: boolean; userId: string; }) { this.accountBalanceItems = accountBalanceItems; @@ -88,6 +89,7 @@ export abstract class PortfolioCalculator { this.currency = currency; this.currentRateService = currentRateService; this.exchangeRateDataService = exchangeRateDataService; + this.filters = filters; let dateOfFirstActivity = new Date(); @@ -128,7 +130,6 @@ export abstract class PortfolioCalculator { }); this.redisCacheService = redisCacheService; - this.useCache = useCache; this.userId = userId; const { endDate, startDate } = getIntervalFromDateRange( @@ -1007,49 +1008,47 @@ export abstract class PortfolioCalculator { } private async initialize() { - if (this.useCache) { - const startTimeTotal = performance.now(); + const startTimeTotal = performance.now(); - const cachedSnapshot = await this.redisCacheService.get( - this.redisCacheService.getPortfolioSnapshotKey({ - userId: this.userId - }) - ); - - if (cachedSnapshot) { - this.snapshot = plainToClass( - PortfolioSnapshot, - JSON.parse(cachedSnapshot) - ); + const cachedSnapshot = await this.redisCacheService.get( + this.redisCacheService.getPortfolioSnapshotKey({ + filters: this.filters, + userId: this.userId + }) + ); - Logger.debug( - `Fetched portfolio snapshot from cache in ${( - (performance.now() - startTimeTotal) / - 1000 - ).toFixed(3)} seconds`, - 'PortfolioCalculator' - ); - } else { - this.snapshot = await this.computeSnapshot(); - - this.redisCacheService.set( - this.redisCacheService.getPortfolioSnapshotKey({ - userId: this.userId - }), - JSON.stringify(this.snapshot), - this.configurationService.get('CACHE_QUOTES_TTL') - ); + if (cachedSnapshot) { + this.snapshot = plainToClass( + PortfolioSnapshot, + JSON.parse(cachedSnapshot) + ); - Logger.debug( - `Computed portfolio snapshot in ${( - (performance.now() - startTimeTotal) / - 1000 - ).toFixed(3)} seconds`, - 'PortfolioCalculator' - ); - } + Logger.debug( + `Fetched portfolio snapshot from cache in ${( + (performance.now() - startTimeTotal) / + 1000 + ).toFixed(3)} seconds`, + 'PortfolioCalculator' + ); } else { this.snapshot = await this.computeSnapshot(); + + this.redisCacheService.set( + this.redisCacheService.getPortfolioSnapshotKey({ + filters: this.filters, + userId: this.userId + }), + JSON.stringify(this.snapshot), + this.configurationService.get('CACHE_QUOTES_TTL') + ); + + Logger.debug( + `Computed portfolio snapshot in ${( + (performance.now() - startTimeTotal) / + 1000 + ).toFixed(3)} seconds`, + 'PortfolioCalculator' + ); } } } diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index 3206a31eb..a83183b16 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -121,7 +121,6 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', - hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts index 912776311..77a5cd852 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -106,7 +106,6 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', - hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts index 6752eef33..c6175c5d0 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-baln-buy.spec.ts @@ -92,7 +92,6 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', - hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 79755ff0a..04484bfe9 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -120,7 +120,6 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', - hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts index d628856dc..194bf47cf 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-fee.spec.ts @@ -91,7 +91,6 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'USD', - hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts index 1177fb461..a4acc2312 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-googl-buy.spec.ts @@ -104,7 +104,6 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', - hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts index d24199af5..0e45d209f 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-item.spec.ts @@ -91,7 +91,6 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'USD', - hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts index f888ee95d..0df8dee48 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-liability.spec.ts @@ -91,7 +91,6 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'USD', - hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts index eea457397..81d0b302a 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -119,7 +119,6 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'USD', - hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts index 67f69d1d2..f939487fd 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-no-orders.spec.ts @@ -69,7 +69,6 @@ describe('PortfolioCalculator', () => { activities: [], calculationType: PerformanceCalculationType.TWR, currency: 'CHF', - hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index 0039db3f8..51d8d141d 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -106,7 +106,6 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', - hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts index 921d87e61..175b519cf 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -106,7 +106,6 @@ describe('PortfolioCalculator', () => { activities, calculationType: PerformanceCalculationType.TWR, currency: 'CHF', - hasFilters: false, userId: userDummyData.id }); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index bf0527ad3..9a0d57314 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -264,12 +264,10 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, + filters, userId, calculationType: PerformanceCalculationType.TWR, - currency: this.request.user.Settings.settings.baseCurrency, - hasFilters: filters?.length > 0, - isExperimentalFeatures: - this.request.user.Settings.settings.isExperimentalFeatures + currency: this.request.user.Settings.settings.baseCurrency }); const { historicalData } = await portfolioCalculator.getSnapshot(); @@ -343,12 +341,10 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, + filters, userId, calculationType: PerformanceCalculationType.TWR, - currency: userCurrency, - hasFilters: filters?.length > 0, // TODO - isExperimentalFeatures: - this.request.user?.Settings.settings.isExperimentalFeatures + currency: userCurrency }); const { currentValueInBaseCurrency, hasErrors, positions } = @@ -652,18 +648,13 @@ export class PortfolioService { { dataSource: aDataSource, symbol: aSymbol } ]); - // TODO: Always use same parameters when calling - // this.calculatorFactory.createCalculator() const portfolioCalculator = this.calculatorFactory.createCalculator({ userId, activities: orders.filter((order) => { return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type); }), calculationType: PerformanceCalculationType.TWR, - currency: userCurrency, - hasFilters: true, - isExperimentalFeatures: - this.request.user.Settings.settings.isExperimentalFeatures + currency: userCurrency }); const portfolioStart = portfolioCalculator.getStartDate(); @@ -933,12 +924,10 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, + filters, userId, calculationType: PerformanceCalculationType.TWR, - currency: this.request.user.Settings.settings.baseCurrency, - hasFilters: filters?.length > 0, - isExperimentalFeatures: - this.request.user.Settings.settings.isExperimentalFeatures + currency: this.request.user.Settings.settings.baseCurrency }); let { hasErrors, positions } = await portfolioCalculator.getSnapshot(); @@ -1125,12 +1114,10 @@ export class PortfolioService { this.calculatorFactory.createCalculator({ accountBalanceItems, activities, + filters, userId, calculationType: PerformanceCalculationType.TWR, - currency: userCurrency, - hasFilters: filters?.length > 0, - isExperimentalFeatures: - this.request.user.Settings.settings.isExperimentalFeatures + currency: userCurrency }); const { errors, hasErrors, historicalData } = @@ -1194,10 +1181,7 @@ export class PortfolioService { activities, userId, calculationType: PerformanceCalculationType.TWR, - currency: this.request.user.Settings.settings.baseCurrency, - hasFilters: false, - isExperimentalFeatures: - this.request.user.Settings.settings.isExperimentalFeatures + currency: this.request.user.Settings.settings.baseCurrency }); let { totalFeesWithCurrencyEffect, positions, totalInvestment } = diff --git a/apps/api/src/app/redis-cache/redis-cache.service.ts b/apps/api/src/app/redis-cache/redis-cache.service.ts index de41220b9..ccdd07c6d 100644 --- a/apps/api/src/app/redis-cache/redis-cache.service.ts +++ b/apps/api/src/app/redis-cache/redis-cache.service.ts @@ -1,9 +1,10 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; -import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; +import { AssetProfileIdentifier, Filter } from '@ghostfolio/common/interfaces'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable, Logger } from '@nestjs/common'; +import { createHash } from 'crypto'; import type { RedisCache } from './interfaces/redis-cache.interface'; @@ -24,8 +25,28 @@ export class RedisCacheService { return this.cache.get(key); } - public getPortfolioSnapshotKey({ userId }: { userId: string }) { - return `portfolio-snapshot-${userId}`; + public async getKeys(aPrefix?: string): Promise { + return this.cache.store.keys(aPrefix); + } + + public getPortfolioSnapshotKey({ + filters, + userId + }: { + filters?: Filter[]; + userId: string; + }) { + let portfolioSnapshotKey = `portfolio-snapshot-${userId}`; + + if (filters?.length > 0) { + const filtersHash = createHash('sha256') + .update(JSON.stringify(filters)) + .digest('hex'); + + portfolioSnapshotKey = `${portfolioSnapshotKey}-${filtersHash}`; + } + + return portfolioSnapshotKey; } public getQuoteKey({ dataSource, symbol }: AssetProfileIdentifier) { @@ -36,6 +57,22 @@ export class RedisCacheService { return this.cache.del(key); } + public async removePortfolioSnapshotsByUserId({ + userId + }: { + userId: string; + }) { + const keys = await this.getKeys( + `${this.getPortfolioSnapshotKey({ userId })}` + ); + + console.log(keys); + + for (const key of keys) { + this.remove(key); + } + } + public async reset() { return this.cache.reset(); } diff --git a/apps/api/src/events/portfolio-changed.listener.ts b/apps/api/src/events/portfolio-changed.listener.ts index fcf47ce6c..d12b9558d 100644 --- a/apps/api/src/events/portfolio-changed.listener.ts +++ b/apps/api/src/events/portfolio-changed.listener.ts @@ -16,10 +16,8 @@ export class PortfolioChangedListener { 'PortfolioChangedListener' ); - this.redisCacheService.remove( - this.redisCacheService.getPortfolioSnapshotKey({ - userId: event.getUserId() - }) - ); + this.redisCacheService.removePortfolioSnapshotsByUserId({ + userId: event.getUserId() + }); } }