diff --git a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts index a90f32c98..5d168b619 100644 --- a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts @@ -1,7 +1,6 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; -import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface'; import { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces'; -import { TimelinePosition } from '@ghostfolio/common/models'; +import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; export class MWRPortfolioCalculator extends PortfolioCalculator { protected calculateOverallPerformance( diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts index 504b5b171..51ad40c31 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator-test-utils.ts @@ -24,3 +24,7 @@ export const symbolProfileDummyData = { sectors: [], updatedAt: undefined }; + +export const userDummyData = { + id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' +}; 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 d0c1ed2cb..881f0dcdd 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts @@ -1,9 +1,10 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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 { DateRange } from '@ghostfolio/common/types'; +import { DateRange, UserWithSettings } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; @@ -19,6 +20,7 @@ export enum PerformanceCalculationType { @Injectable() export class PortfolioCalculatorFactory { public constructor( + private readonly configurationService: ConfigurationService, private readonly currentRateService: CurrentRateService, private readonly exchangeRateDataService: ExchangeRateDataService, private readonly redisCacheService: RedisCacheService @@ -29,13 +31,15 @@ export class PortfolioCalculatorFactory { activities, calculationType, currency, - dateRange = 'max' + dateRange = 'max', + userId }: { accountBalanceItems?: HistoricalDataItem[]; activities: Activity[]; calculationType: PerformanceCalculationType; currency: string; dateRange?: DateRange; + userId: string; }): PortfolioCalculator { switch (calculationType) { case PerformanceCalculationType.MWR: @@ -44,6 +48,8 @@ export class PortfolioCalculatorFactory { activities, currency, dateRange, + userId, + configurationService: this.configurationService, currentRateService: this.currentRateService, exchangeRateDataService: this.exchangeRateDataService, redisCacheService: this.redisCacheService @@ -55,6 +61,8 @@ export class PortfolioCalculatorFactory { currency, currentRateService: this.currentRateService, dateRange, + userId, + configurationService: this.configurationService, exchangeRateDataService: this.exchangeRateDataService, redisCacheService: this.redisCacheService }); diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index bc4bd1e47..82ebd6611 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -1,7 +1,6 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; -import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface'; import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; @@ -9,6 +8,7 @@ import { getFactor, getInterval } from '@ghostfolio/api/helper/portfolio.helper'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { MAX_CHART_ITEMS } from '@ghostfolio/common/config'; @@ -26,10 +26,11 @@ import { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces'; -import { TimelinePosition } from '@ghostfolio/common/models'; +import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; import { DateRange, GroupBy } from '@ghostfolio/common/types'; import { Big } from 'big.js'; +import { plainToClass } from 'class-transformer'; import { differenceInDays, eachDayOfInterval, @@ -42,6 +43,7 @@ import { subDays } from 'date-fns'; import { first, last, uniq, uniqBy } from 'lodash'; +import ms from 'ms'; export abstract class PortfolioCalculator { protected static readonly ENABLE_LOGGING = false; @@ -49,53 +51,74 @@ export abstract class PortfolioCalculator { protected accountBalanceItems: HistoricalDataItem[]; protected orders: PortfolioOrder[]; + private configurationService: ConfigurationService; private currency: string; private currentRateService: CurrentRateService; private dataProviderInfos: DataProviderInfo[]; private endDate: Date; private exchangeRateDataService: ExchangeRateDataService; + private redisCacheService: RedisCacheService; private snapshot: PortfolioSnapshot; private snapshotPromise: Promise; private startDate: Date; private transactionPoints: TransactionPoint[]; + private userId: string; public constructor({ accountBalanceItems, activities, + configurationService, currency, currentRateService, dateRange, - exchangeRateDataService + exchangeRateDataService, + redisCacheService, + userId }: { accountBalanceItems: HistoricalDataItem[]; activities: Activity[]; + configurationService: ConfigurationService; currency: string; currentRateService: CurrentRateService; dateRange: DateRange; exchangeRateDataService: ExchangeRateDataService; redisCacheService: RedisCacheService; + userId: string; }) { this.accountBalanceItems = accountBalanceItems; + this.configurationService = configurationService; this.currency = currency; this.currentRateService = currentRateService; this.exchangeRateDataService = exchangeRateDataService; - this.orders = activities.map( - ({ date, fee, quantity, SymbolProfile, tags = [], type, unitPrice }) => { - return { + + this.orders = activities + .map( + ({ + date, + fee, + quantity, SymbolProfile, - tags, + tags = [], type, - date: format(date, DATE_FORMAT), - fee: new Big(fee), - quantity: new Big(quantity), - unitPrice: new Big(unitPrice) - }; - } - ); + unitPrice + }) => { + return { + SymbolProfile, + tags, + type, + date: format(date, DATE_FORMAT), + fee: new Big(fee), + quantity: new Big(quantity), + unitPrice: new Big(unitPrice) + }; + } + ) + .sort((a, b) => { + return a.date?.localeCompare(b.date); + }); - this.orders.sort((a, b) => { - return a.date?.localeCompare(b.date); - }); + this.redisCacheService = redisCacheService; + this.userId = userId; const { endDate, startDate } = getInterval(dateRange); @@ -1013,6 +1036,23 @@ export abstract class PortfolioCalculator { } private async initialize() { - this.snapshot = await this.computeSnapshot(this.startDate, this.endDate); + const cachedSnapshot = await this.redisCacheService.get( + this.redisCacheService.getPortfolioSnapshotKey(this.userId) + ); + + if (cachedSnapshot) { + this.snapshot = plainToClass( + PortfolioSnapshot, + JSON.parse(cachedSnapshot) + ); + } else { + this.snapshot = await this.computeSnapshot(this.startDate, this.endDate); + + this.redisCacheService.set( + this.redisCacheService.getPortfolioSnapshotKey(this.userId), + JSON.stringify(this.snapshot), + this.configurationService.get('CACHE_QUOTES_TTL') + ); + } } } 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 790e121ac..422cf8bff 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 @@ -1,7 +1,8 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - symbolProfileDummyData + symbolProfileDummyData, + userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory, @@ -11,6 +12,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { parseDate } from '@ghostfolio/common/helper'; @@ -35,12 +37,15 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -53,6 +58,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService @@ -116,7 +122,8 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = factory.createCalculator({ activities, calculationType: PerformanceCalculationType.TWR, - currency: 'CHF' + currency: 'CHF', + userId: userDummyData.id }); const chartData = await portfolioCalculator.getChartData({ 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 fef8220fe..dee8b2478 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 @@ -1,7 +1,8 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - symbolProfileDummyData + symbolProfileDummyData, + userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PerformanceCalculationType, @@ -11,6 +12,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { parseDate } from '@ghostfolio/common/helper'; @@ -35,12 +37,15 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -53,6 +58,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService @@ -101,7 +107,8 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = factory.createCalculator({ activities, calculationType: PerformanceCalculationType.TWR, - currency: 'CHF' + currency: 'CHF', + userId: userDummyData.id }); const chartData = await portfolioCalculator.getChartData({ 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 85f981eed..db8ce01b3 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 @@ -1,7 +1,8 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - symbolProfileDummyData + symbolProfileDummyData, + userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory, @@ -11,6 +12,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { parseDate } from '@ghostfolio/common/helper'; @@ -35,12 +37,15 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -53,6 +58,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService @@ -86,7 +92,8 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = factory.createCalculator({ activities, calculationType: PerformanceCalculationType.TWR, - currency: 'CHF' + currency: 'CHF', + userId: userDummyData.id }); const chartData = await portfolioCalculator.getChartData({ 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 fa3aa7493..5a403eda1 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 @@ -1,7 +1,8 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - symbolProfileDummyData + symbolProfileDummyData, + userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory, @@ -11,6 +12,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; import { parseDate } from '@ghostfolio/common/helper'; @@ -48,12 +50,15 @@ jest.mock( ); describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -66,6 +71,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService @@ -114,7 +120,8 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = factory.createCalculator({ activities, calculationType: PerformanceCalculationType.TWR, - currency: 'CHF' + currency: 'CHF', + userId: userDummyData.id }); const chartData = await portfolioCalculator.getChartData({ 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 16d10f848..97a77492b 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 @@ -1,7 +1,8 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - symbolProfileDummyData + symbolProfileDummyData, + userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory, @@ -11,6 +12,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { parseDate } from '@ghostfolio/common/helper'; @@ -35,12 +37,15 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -53,6 +58,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService @@ -86,7 +92,8 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = factory.createCalculator({ activities, calculationType: PerformanceCalculationType.TWR, - currency: 'USD' + currency: 'USD', + userId: userDummyData.id }); const portfolioSnapshot = await portfolioCalculator.computeSnapshot( 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 f57b7125a..c916a381d 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 @@ -1,7 +1,8 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - symbolProfileDummyData + symbolProfileDummyData, + userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory, @@ -11,6 +12,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; import { parseDate } from '@ghostfolio/common/helper'; @@ -48,12 +50,15 @@ jest.mock( ); describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -66,6 +71,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService @@ -99,7 +105,8 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = factory.createCalculator({ activities, calculationType: PerformanceCalculationType.TWR, - currency: 'CHF' + currency: 'CHF', + userId: userDummyData.id }); const chartData = await portfolioCalculator.getChartData({ 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 50eaabbf2..bf212f80b 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 @@ -1,7 +1,8 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - symbolProfileDummyData + symbolProfileDummyData, + userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory, @@ -11,6 +12,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { parseDate } from '@ghostfolio/common/helper'; @@ -35,12 +37,15 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -53,6 +58,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService @@ -86,7 +92,8 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = factory.createCalculator({ activities, calculationType: PerformanceCalculationType.TWR, - currency: 'USD' + currency: 'USD', + userId: userDummyData.id }); const portfolioSnapshot = await portfolioCalculator.computeSnapshot( 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 a13d820a7..fc858f293 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 @@ -1,7 +1,8 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - symbolProfileDummyData + symbolProfileDummyData, + userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory, @@ -11,6 +12,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { parseDate } from '@ghostfolio/common/helper'; @@ -35,12 +37,15 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -53,6 +58,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService @@ -86,7 +92,8 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = factory.createCalculator({ activities, calculationType: PerformanceCalculationType.TWR, - currency: 'USD' + currency: 'USD', + userId: userDummyData.id }); const portfolioSnapshot = await portfolioCalculator.computeSnapshot( 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 ef6e934ba..6948b4dbd 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 @@ -1,7 +1,8 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - symbolProfileDummyData + symbolProfileDummyData, + userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PerformanceCalculationType, @@ -11,6 +12,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; import { parseDate } from '@ghostfolio/common/helper'; @@ -48,12 +50,15 @@ jest.mock( ); describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -66,6 +71,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService @@ -114,7 +120,8 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = factory.createCalculator({ activities, calculationType: PerformanceCalculationType.TWR, - currency: 'USD' + currency: 'USD', + userId: userDummyData.id }); const portfolioSnapshot = await portfolioCalculator.computeSnapshot( 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 b6b2156ce..1dd74d7e5 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 @@ -1,3 +1,4 @@ +import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PerformanceCalculationType, PortfolioCalculatorFactory @@ -6,6 +7,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { parseDate } from '@ghostfolio/common/helper'; @@ -31,12 +33,15 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -49,6 +54,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService @@ -64,7 +70,8 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = factory.createCalculator({ activities: [], calculationType: PerformanceCalculationType.TWR, - currency: 'CHF' + currency: 'CHF', + userId: userDummyData.id }); const start = subDays(new Date(Date.now()), 10); 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 f74f00159..d4451503a 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 @@ -1,7 +1,8 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - symbolProfileDummyData + symbolProfileDummyData, + userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PerformanceCalculationType, @@ -11,6 +12,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { parseDate } from '@ghostfolio/common/helper'; @@ -35,12 +37,15 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -53,6 +58,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService @@ -101,7 +107,8 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = factory.createCalculator({ activities, calculationType: PerformanceCalculationType.TWR, - currency: 'CHF' + currency: 'CHF', + userId: userDummyData.id }); const chartData = await portfolioCalculator.getChartData({ 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 3f52eeed4..7850fb2bd 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 @@ -1,7 +1,8 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { activityDummyData, - symbolProfileDummyData + symbolProfileDummyData, + userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PerformanceCalculationType, @@ -11,6 +12,7 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.s import { CurrentRateServiceMock } from '@ghostfolio/api/app/portfolio/current-rate.service.mock'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { RedisCacheServiceMock } from '@ghostfolio/api/app/redis-cache/redis-cache.service.mock'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { parseDate } from '@ghostfolio/common/helper'; @@ -35,12 +37,15 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -53,6 +58,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService @@ -101,7 +107,8 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = factory.createCalculator({ activities, calculationType: PerformanceCalculationType.TWR, - currency: 'CHF' + currency: 'CHF', + userId: userDummyData.id }); const chartData = await portfolioCalculator.getChartData({ diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts index 5f5e4d107..536581070 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.spec.ts @@ -1,15 +1,19 @@ import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; let currentRateService: CurrentRateService; let exchangeRateDataService: ExchangeRateDataService; let factory: PortfolioCalculatorFactory; let redisCacheService: RedisCacheService; beforeEach(() => { + configurationService = new ConfigurationService(); + currentRateService = new CurrentRateService(null, null, null, null); exchangeRateDataService = new ExchangeRateDataService( @@ -22,6 +26,7 @@ describe('PortfolioCalculator', () => { redisCacheService = new RedisCacheService(null, null); factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, exchangeRateDataService, redisCacheService 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 62d36aebb..f480fdc59 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -1,10 +1,9 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { PortfolioOrderItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order-item.interface'; -import { PortfolioSnapshot } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-snapshot.interface'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces'; -import { TimelinePosition } from '@ghostfolio/common/models'; +import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; import { Logger } from '@nestjs/common'; import { Big } from 'big.js'; diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts deleted file mode 100644 index f8bba2113..000000000 --- a/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ResponseError } from '@ghostfolio/common/interfaces'; -import { TimelinePosition } from '@ghostfolio/common/models'; - -import { Big } from 'big.js'; - -export interface PortfolioSnapshot extends ResponseError { - currentValueInBaseCurrency: Big; - grossPerformance: Big; - grossPerformanceWithCurrencyEffect: Big; - grossPerformancePercentage: Big; - grossPerformancePercentageWithCurrencyEffect: Big; - netAnnualizedPerformance?: Big; - netAnnualizedPerformanceWithCurrencyEffect?: Big; - netPerformance: Big; - netPerformanceWithCurrencyEffect: Big; - netPerformancePercentage: Big; - netPerformancePercentageWithCurrencyEffect: Big; - positions: TimelinePosition[]; - totalFeesWithCurrencyEffect: Big; - totalInterestWithCurrencyEffect: Big; - totalInvestment: Big; - totalInvestmentWithCurrencyEffect: Big; - totalLiabilitiesWithCurrencyEffect: Big; - totalValuablesWithCurrencyEffect: Big; -} diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index a724e5d98..0bb513ec2 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -277,6 +277,7 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, + userId, calculationType: PerformanceCalculationType.TWR, currency: this.request.user.Settings.settings.baseCurrency }); @@ -352,6 +353,7 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, dateRange, + userId, calculationType: PerformanceCalculationType.TWR, currency: userCurrency }); @@ -648,6 +650,7 @@ export class PortfolioService { ]); const portfolioCalculator = this.calculatorFactory.createCalculator({ + userId, activities: orders.filter((order) => { return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type); }), @@ -919,6 +922,7 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, dateRange, + userId, calculationType: PerformanceCalculationType.TWR, currency: this.request.user.Settings.settings.baseCurrency }); @@ -1108,6 +1112,7 @@ export class PortfolioService { accountBalanceItems, activities, dateRange, + userId, calculationType: PerformanceCalculationType.TWR, currency: userCurrency }); @@ -1202,6 +1207,7 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, + userId, calculationType: PerformanceCalculationType.TWR, currency: this.request.user.Settings.settings.baseCurrency }); diff --git a/apps/api/src/app/redis-cache/redis-cache.service.mock.ts b/apps/api/src/app/redis-cache/redis-cache.service.mock.ts index d65748175..2422e88ab 100644 --- a/apps/api/src/app/redis-cache/redis-cache.service.mock.ts +++ b/apps/api/src/app/redis-cache/redis-cache.service.mock.ts @@ -1 +1,13 @@ -export const RedisCacheServiceMock = {}; +import { RedisCacheService } from './redis-cache.service'; + +export const RedisCacheServiceMock = { + get: (key: string): Promise => { + return Promise.resolve(null); + }, + getPortfolioSnapshotKey: (userId: string): string => { + return `portfolio-snapshot-${userId}`; + }, + set: (key: string, value: string, ttlInSeconds?: number): Promise => { + return Promise.resolve(value); + } +}; 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 3891cc5ab..a313eadf1 100644 --- a/apps/api/src/app/redis-cache/redis-cache.service.ts +++ b/apps/api/src/app/redis-cache/redis-cache.service.ts @@ -24,6 +24,10 @@ export class RedisCacheService { return this.cache.get(key); } + public getPortfolioSnapshotKey(userId: string) { + return `portfolio-snapshot-${userId}`; + } + public getQuoteKey({ dataSource, symbol }: UniqueAsset) { return `quote-${getAssetProfileIdentifier({ dataSource, symbol })}`; } diff --git a/apps/api/src/events/events.module.ts b/apps/api/src/events/events.module.ts index bf9708f4b..0e6b25ba4 100644 --- a/apps/api/src/events/events.module.ts +++ b/apps/api/src/events/events.module.ts @@ -1,8 +1,11 @@ +import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; + import { Module } from '@nestjs/common'; import { PortfolioChangedListener } from './portfolio-changed.listener'; @Module({ + imports: [RedisCacheModule], providers: [PortfolioChangedListener] }) export class EventsModule {} diff --git a/apps/api/src/events/portfolio-changed.listener.ts b/apps/api/src/events/portfolio-changed.listener.ts index 3dd856084..0f8877127 100644 --- a/apps/api/src/events/portfolio-changed.listener.ts +++ b/apps/api/src/events/portfolio-changed.listener.ts @@ -1,3 +1,5 @@ +import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; + import { Injectable, Logger } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; @@ -5,11 +7,17 @@ import { PortfolioChangedEvent } from './portfolio-changed.event'; @Injectable() export class PortfolioChangedListener { + public constructor(private readonly redisCacheService: RedisCacheService) {} + @OnEvent(PortfolioChangedEvent.getName()) handlePortfolioChangedEvent(event: PortfolioChangedEvent) { Logger.log( `Portfolio of user with id ${event.getUserId()} has changed`, 'PortfolioChangedListener' ); + + this.redisCacheService.remove( + this.redisCacheService.getPortfolioSnapshotKey(event.getUserId()) + ); } } diff --git a/libs/common/src/lib/models/index.ts b/libs/common/src/lib/models/index.ts index e5f81fe3b..0dd601a0e 100644 --- a/libs/common/src/lib/models/index.ts +++ b/libs/common/src/lib/models/index.ts @@ -1,3 +1,4 @@ +import { PortfolioSnapshot } from './portfolio-snapshot'; import { TimelinePosition } from './timeline-position'; -export { TimelinePosition }; +export { PortfolioSnapshot, TimelinePosition }; diff --git a/libs/common/src/lib/models/portfolio-snapshot.ts b/libs/common/src/lib/models/portfolio-snapshot.ts new file mode 100644 index 000000000..d0a2c184b --- /dev/null +++ b/libs/common/src/lib/models/portfolio-snapshot.ts @@ -0,0 +1,89 @@ +import { transformToBig } from '@ghostfolio/common/class-transformer'; +import { UniqueAsset } from '@ghostfolio/common/interfaces'; +import { TimelinePosition } from '@ghostfolio/common/models'; + +import { Big } from 'big.js'; +import { Transform, Type, plainToClass } from 'class-transformer'; + +export class PortfolioSnapshot { + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + currentValueInBaseCurrency: Big; + errors?: UniqueAsset[]; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + grossPerformance: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + grossPerformanceWithCurrencyEffect: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + grossPerformancePercentage: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + grossPerformancePercentageWithCurrencyEffect: Big; + + hasErrors: boolean; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + netAnnualizedPerformance?: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + netAnnualizedPerformanceWithCurrencyEffect?: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + netPerformance: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + netPerformanceWithCurrencyEffect: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + netPerformancePercentage: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + netPerformancePercentageWithCurrencyEffect: Big; + + /*@Transform( + ({ value }) => + value.map((position: any) => { + return plainToClass(TimelinePosition, position); + }), + { toClassOnly: true } + )*/ + @Type(() => TimelinePosition) + positions: TimelinePosition[]; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + totalFeesWithCurrencyEffect: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + totalInterestWithCurrencyEffect: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + totalInvestment: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + totalInvestmentWithCurrencyEffect: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + totalLiabilitiesWithCurrencyEffect: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + totalValuablesWithCurrencyEffect: Big; +}