From 4f41bac3288595ec9a8e3190aeadffd30df3892b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 27 Apr 2024 15:35:28 +0200 Subject: [PATCH] Feature/set up caching in portfolio calculator (#3335) * Set up caching * Update changelog --- CHANGELOG.md | 2 +- .../calculator/mwr/portfolio-calculator.ts | 8 +- .../portfolio-calculator-test-utils.ts | 4 + .../portfolio-calculator.factory.ts | 26 ++++- .../calculator/portfolio-calculator.ts | 108 +++++++++++++++--- ...aln-buy-and-sell-in-two-activities.spec.ts | 28 ++++- ...folio-calculator-baln-buy-and-sell.spec.ts | 28 ++++- .../twr/portfolio-calculator-baln-buy.spec.ts | 28 ++++- ...ator-btcusd-buy-and-sell-partially.spec.ts | 28 ++++- .../twr/portfolio-calculator-fee.spec.ts | 28 ++++- .../portfolio-calculator-googl-buy.spec.ts | 28 ++++- .../twr/portfolio-calculator-item.spec.ts | 28 ++++- .../portfolio-calculator-liability.spec.ts | 28 ++++- ...-calculator-msft-buy-with-dividend.spec.ts | 28 ++++- .../portfolio-calculator-no-orders.spec.ts | 26 ++++- ...ulator-novn-buy-and-sell-partially.spec.ts | 28 ++++- ...folio-calculator-novn-buy-and-sell.spec.ts | 28 ++++- .../twr/portfolio-calculator.spec.ts | 12 +- .../calculator/twr/portfolio-calculator.ts | 8 +- .../portfolio-snapshot.interface.ts | 24 ---- .../api/src/app/portfolio/portfolio.module.ts | 2 + .../src/app/portfolio/portfolio.service.ts | 34 ++++-- .../redis-cache/redis-cache.service.mock.ts | 13 +++ .../app/redis-cache/redis-cache.service.ts | 4 + apps/api/src/events/events.module.ts | 3 + .../src/events/portfolio-changed.listener.ts | 8 ++ apps/api/src/models/rule.ts | 3 +- .../base-currency-current-investment.ts | 3 +- .../current-investment.ts | 3 +- .../data-provider/data-provider.service.ts | 13 ++- libs/common/src/lib/class-transformer.ts | 9 ++ libs/common/src/lib/interfaces/index.ts | 2 - .../interfaces/timeline-position.interface.ts | 31 ----- libs/common/src/lib/models/index.ts | 4 + .../src/lib/models/portfolio-snapshot.ts | 82 +++++++++++++ .../src/lib/models/timeline-position.ts | 92 +++++++++++++++ 36 files changed, 687 insertions(+), 145 deletions(-) delete mode 100644 apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts create mode 100644 apps/api/src/app/redis-cache/redis-cache.service.mock.ts create mode 100644 libs/common/src/lib/class-transformer.ts delete mode 100644 libs/common/src/lib/interfaces/timeline-position.interface.ts create mode 100644 libs/common/src/lib/models/index.ts create mode 100644 libs/common/src/lib/models/portfolio-snapshot.ts create mode 100644 libs/common/src/lib/models/timeline-position.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e558f5afd..d718476d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Extended the content of the _Self-Hosting_ section by the custom asset instructions on the Frequently Asked Questions (FAQ) page -- Set up an event system to follow portfolio changes +- Added the caching to the portfolio calculator (experimental) ### Changed 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 978f1f3aa..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,10 +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, - TimelinePosition, - UniqueAsset -} from '@ghostfolio/common/interfaces'; +import { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces'; +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 a75ce9b62..4937f1008 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts @@ -1,8 +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'; @@ -18,8 +20,10 @@ export enum PerformanceCalculationType { @Injectable() export class PortfolioCalculatorFactory { public constructor( + private readonly configurationService: ConfigurationService, private readonly currentRateService: CurrentRateService, - private readonly exchangeRateDataService: ExchangeRateDataService + private readonly exchangeRateDataService: ExchangeRateDataService, + private readonly redisCacheService: RedisCacheService ) {} public createCalculator({ @@ -27,13 +31,17 @@ export class PortfolioCalculatorFactory { activities, calculationType, currency, - dateRange = 'max' + dateRange = 'max', + isExperimentalFeatures = false, + userId }: { accountBalanceItems?: HistoricalDataItem[]; activities: Activity[]; calculationType: PerformanceCalculationType; currency: string; dateRange?: DateRange; + isExperimentalFeatures?: boolean; + userId: string; }): PortfolioCalculator { switch (calculationType) { case PerformanceCalculationType.MWR: @@ -42,8 +50,12 @@ export class PortfolioCalculatorFactory { activities, currency, dateRange, + isExperimentalFeatures, + userId, + configurationService: this.configurationService, currentRateService: this.currentRateService, - exchangeRateDataService: this.exchangeRateDataService + exchangeRateDataService: this.exchangeRateDataService, + redisCacheService: this.redisCacheService }); case PerformanceCalculationType.TWR: return new TWRPortfolioCalculator({ @@ -52,7 +64,11 @@ export class PortfolioCalculatorFactory { currency, currentRateService: this.currentRateService, dateRange, - exchangeRateDataService: this.exchangeRateDataService + isExperimentalFeatures, + userId, + configurationService: this.configurationService, + exchangeRateDataService: this.exchangeRateDataService, + redisCacheService: this.redisCacheService }); default: throw new Error('Invalid calculation type'); diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 54e474779..ba9833948 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -1,13 +1,14 @@ 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'; 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'; @@ -23,12 +24,14 @@ import { InvestmentItem, ResponseError, SymbolMetrics, - TimelinePosition, UniqueAsset } from '@ghostfolio/common/interfaces'; +import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; import { DateRange, GroupBy } from '@ghostfolio/common/types'; +import { Logger } from '@nestjs/common'; import { Big } from 'big.js'; +import { plainToClass } from 'class-transformer'; import { differenceInDays, eachDayOfInterval, @@ -41,6 +44,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; @@ -48,52 +52,78 @@ 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 isExperimentalFeatures: boolean; + 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, + isExperimentalFeatures, + redisCacheService, + userId }: { accountBalanceItems: HistoricalDataItem[]; activities: Activity[]; + configurationService: ConfigurationService; currency: string; currentRateService: CurrentRateService; dateRange: DateRange; exchangeRateDataService: ExchangeRateDataService; + isExperimentalFeatures: boolean; + 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.isExperimentalFeatures = isExperimentalFeatures; + + 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); @@ -1011,6 +1041,48 @@ export abstract class PortfolioCalculator { } private async initialize() { - this.snapshot = await this.computeSnapshot(this.startDate, this.endDate); + if (this.isExperimentalFeatures) { + const startTimeTotal = performance.now(); + + const cachedSnapshot = await this.redisCacheService.get( + this.redisCacheService.getPortfolioSnapshotKey(this.userId) + ); + + if (cachedSnapshot) { + this.snapshot = plainToClass( + PortfolioSnapshot, + JSON.parse(cachedSnapshot) + ); + + Logger.debug( + `Fetched portfolio snapshot from cache in ${( + (performance.now() - startTimeTotal) / + 1000 + ).toFixed(3)} seconds`, + 'PortfolioCalculator' + ); + } 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') + ); + + Logger.debug( + `Computed portfolio snapshot in ${( + (performance.now() - startTimeTotal) / + 1000 + ).toFixed(3)} seconds`, + 'PortfolioCalculator' + ); + } + } else { + this.snapshot = await this.computeSnapshot(this.startDate, this.endDate); + } } } 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 a11ae8896..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, @@ -9,6 +10,9 @@ import { } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; @@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + 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( @@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + exchangeRateDataService, + redisCacheService ); }); @@ -101,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 8d93d8b97..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, @@ -9,6 +10,9 @@ import { } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; @@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + 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( @@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + exchangeRateDataService, + redisCacheService ); }); @@ -86,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 f26331134..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, @@ -9,6 +10,9 @@ import { } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; @@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + 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( @@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + exchangeRateDataService, + redisCacheService ); }); @@ -71,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 2a9ba0916..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, @@ -9,6 +10,9 @@ import { } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; @@ -24,6 +28,15 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + jest.mock( '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', () => { @@ -37,11 +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( @@ -51,9 +68,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + exchangeRateDataService, + redisCacheService ); }); @@ -99,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 83f99e3cb..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, @@ -9,6 +10,9 @@ import { } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; @@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + 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( @@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + exchangeRateDataService, + redisCacheService ); }); @@ -71,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 0642b28ed..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, @@ -9,6 +10,9 @@ import { } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; @@ -24,6 +28,15 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + jest.mock( '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', () => { @@ -37,11 +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( @@ -51,9 +68,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + exchangeRateDataService, + redisCacheService ); }); @@ -84,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 b8ef6954e..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, @@ -9,6 +10,9 @@ import { } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; @@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + 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( @@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + exchangeRateDataService, + redisCacheService ); }); @@ -71,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 9ef369c8f..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, @@ -9,6 +10,9 @@ import { } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; @@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + 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( @@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + exchangeRateDataService, + redisCacheService ); }); @@ -71,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 e50ce4194..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, @@ -9,6 +10,9 @@ import { } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; @@ -24,6 +28,15 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + jest.mock( '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', () => { @@ -37,11 +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( @@ -51,9 +68,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + exchangeRateDataService, + redisCacheService ); }); @@ -99,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 bd04d6045..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,9 +1,13 @@ +import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PerformanceCalculationType, PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; @@ -19,12 +23,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + 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( @@ -34,9 +51,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + exchangeRateDataService, + redisCacheService ); }); @@ -49,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 3d63f1a5d..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, @@ -9,6 +10,9 @@ import { } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; @@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + 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( @@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + exchangeRateDataService, + redisCacheService ); }); @@ -86,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 0dd16b045..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, @@ -9,6 +10,9 @@ import { } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; 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'; @@ -23,12 +27,25 @@ jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { }; }); +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + 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( @@ -38,9 +55,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + exchangeRateDataService, + redisCacheService ); }); @@ -86,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 365593846..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,13 +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( @@ -17,9 +23,13 @@ describe('PortfolioCalculator', () => { null ); + redisCacheService = new RedisCacheService(null, null); + factory = new PortfolioCalculatorFactory( + configurationService, currentRateService, - exchangeRateDataService + 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 9458fb1bd..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,13 +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, - TimelinePosition, - UniqueAsset -} from '@ghostfolio/common/interfaces'; +import { SymbolMetrics, UniqueAsset } from '@ghostfolio/common/interfaces'; +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 d89734987..000000000 --- a/apps/api/src/app/portfolio/interfaces/portfolio-snapshot.interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ResponseError, TimelinePosition } from '@ghostfolio/common/interfaces'; - -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.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index 6b06bf02d..5659f2a7e 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -2,6 +2,7 @@ import { AccessModule } from '@ghostfolio/api/app/access/access.module'; import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { OrderModule } from '@ghostfolio/api/app/order/order.module'; +import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; @@ -35,6 +36,7 @@ import { RulesService } from './rules.service'; MarketDataModule, OrderModule, PrismaModule, + RedisCacheModule, SymbolProfileModule, UserModule ], diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 4e56f844b..a7e80ce1c 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -29,6 +29,7 @@ import { EnhancedSymbolProfile, Filter, HistoricalDataItem, + InvestmentItem, PortfolioDetails, PortfolioInvestments, PortfolioPerformanceResponse, @@ -36,10 +37,9 @@ import { PortfolioReport, PortfolioSummary, Position, - TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces'; -import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; +import { TimelinePosition } from '@ghostfolio/common/models'; import type { AccountWithValue, DateRange, @@ -277,8 +277,11 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, + userId, calculationType: PerformanceCalculationType.TWR, - currency: this.request.user.Settings.settings.baseCurrency + currency: this.request.user.Settings.settings.baseCurrency, + isExperimentalFeatures: + this.request.user.Settings.settings.isExperimentalFeatures }); const items = await portfolioCalculator.getChart({ @@ -352,8 +355,11 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, dateRange, + userId, calculationType: PerformanceCalculationType.TWR, - currency: userCurrency + currency: userCurrency, + isExperimentalFeatures: + this.request.user.Settings.settings.isExperimentalFeatures }); const { currentValueInBaseCurrency, hasErrors, positions } = @@ -648,11 +654,14 @@ export class PortfolioService { ]); const portfolioCalculator = this.calculatorFactory.createCalculator({ + userId, activities: orders.filter((order) => { return ['BUY', 'DIVIDEND', 'ITEM', 'SELL'].includes(order.type); }), calculationType: PerformanceCalculationType.TWR, - currency: userCurrency + currency: userCurrency, + isExperimentalFeatures: + this.request.user.Settings.settings.isExperimentalFeatures }); const portfolioStart = portfolioCalculator.getStartDate(); @@ -919,8 +928,11 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, dateRange, + userId, calculationType: PerformanceCalculationType.TWR, - currency: this.request.user.Settings.settings.baseCurrency + currency: this.request.user.Settings.settings.baseCurrency, + isExperimentalFeatures: + this.request.user.Settings.settings.isExperimentalFeatures }); let { hasErrors, positions } = await portfolioCalculator.getSnapshot(); @@ -1108,8 +1120,11 @@ export class PortfolioService { accountBalanceItems, activities, dateRange, + userId, calculationType: PerformanceCalculationType.TWR, - currency: userCurrency + currency: userCurrency, + isExperimentalFeatures: + this.request.user.Settings.settings.isExperimentalFeatures }); const { @@ -1202,8 +1217,11 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, + userId, calculationType: PerformanceCalculationType.TWR, - currency: this.request.user.Settings.settings.baseCurrency + currency: this.request.user.Settings.settings.baseCurrency, + isExperimentalFeatures: + this.request.user.Settings.settings.isExperimentalFeatures }); let { totalFeesWithCurrencyEffect, positions, totalInvestment } = 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 new file mode 100644 index 000000000..2422e88ab --- /dev/null +++ b/apps/api/src/app/redis-cache/redis-cache.service.mock.ts @@ -0,0 +1,13 @@ +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/apps/api/src/models/rule.ts b/apps/api/src/models/rule.ts index ba37f4e94..8397f3e46 100644 --- a/apps/api/src/models/rule.ts +++ b/apps/api/src/models/rule.ts @@ -1,7 +1,8 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { groupBy } from '@ghostfolio/common/helper'; -import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces'; +import { UserSettings } from '@ghostfolio/common/interfaces'; +import { TimelinePosition } from '@ghostfolio/common/models'; import { EvaluationResult } from './interfaces/evaluation-result.interface'; import { RuleInterface } from './interfaces/rule.interface'; diff --git a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts index 39406e6c2..372250dbc 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts @@ -1,7 +1,8 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { Rule } from '@ghostfolio/api/models/rule'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces'; +import { UserSettings } from '@ghostfolio/common/interfaces'; +import { TimelinePosition } from '@ghostfolio/common/models'; export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { private positions: TimelinePosition[]; diff --git a/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts index 078aaba9b..8ebb24ac0 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts @@ -1,7 +1,8 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { Rule } from '@ghostfolio/api/models/rule'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { TimelinePosition, UserSettings } from '@ghostfolio/common/interfaces'; +import { UserSettings } from '@ghostfolio/common/interfaces'; +import { TimelinePosition } from '@ghostfolio/common/models'; export class CurrencyClusterRiskCurrentInvestment extends Rule { private positions: TimelinePosition[]; diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index 675c13377..26fc171b5 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -399,7 +399,8 @@ export class DataProviderService { numberOfItemsInCache > 1 ? 's' : '' } from cache in ${((performance.now() - startTimeTotal) / 1000).toFixed( 3 - )} seconds` + )} seconds`, + 'DataProviderService' ); } @@ -505,7 +506,8 @@ export class DataProviderService { } from ${dataSource} in ${( (performance.now() - startTimeDataSource) / 1000 - ).toFixed(3)} seconds` + ).toFixed(3)} seconds`, + 'DataProviderService' ); try { @@ -535,14 +537,15 @@ export class DataProviderService { await Promise.all(promises); - Logger.debug('------------------------------------------------'); + Logger.debug('--------------------------------------------------------'); Logger.debug( `Fetched ${items.length} quote${items.length > 1 ? 's' : ''} in ${( (performance.now() - startTimeTotal) / 1000 - ).toFixed(3)} seconds` + ).toFixed(3)} seconds`, + 'DataProviderService' ); - Logger.debug('================================================'); + Logger.debug('========================================================'); return response; } diff --git a/libs/common/src/lib/class-transformer.ts b/libs/common/src/lib/class-transformer.ts new file mode 100644 index 000000000..bd9db22da --- /dev/null +++ b/libs/common/src/lib/class-transformer.ts @@ -0,0 +1,9 @@ +import { Big } from 'big.js'; + +export function transformToBig({ value }: { value: string }): Big { + if (value === null) { + return null; + } + + return new Big(value); +} diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index dba1ac79a..c2c9ce619 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -48,7 +48,6 @@ import type { Subscription } from './subscription.interface'; import type { SymbolMetrics } from './symbol-metrics.interface'; import type { SystemMessage } from './system-message.interface'; import type { TabConfiguration } from './tab-configuration.interface'; -import type { TimelinePosition } from './timeline-position.interface'; import type { UniqueAsset } from './unique-asset.interface'; import type { UserSettings } from './user-settings.interface'; import type { User } from './user.interface'; @@ -102,7 +101,6 @@ export { Subscription, SymbolMetrics, TabConfiguration, - TimelinePosition, UniqueAsset, User, UserSettings diff --git a/libs/common/src/lib/interfaces/timeline-position.interface.ts b/libs/common/src/lib/interfaces/timeline-position.interface.ts deleted file mode 100644 index 539f887ce..000000000 --- a/libs/common/src/lib/interfaces/timeline-position.interface.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { DataSource, Tag } from '@prisma/client'; -import { Big } from 'big.js'; - -export interface TimelinePosition { - averagePrice: Big; - currency: string; - dataSource: DataSource; - dividend: Big; - dividendInBaseCurrency: Big; - fee: Big; - firstBuyDate: string; - grossPerformance: Big; - grossPerformancePercentage: Big; - grossPerformancePercentageWithCurrencyEffect: Big; - grossPerformanceWithCurrencyEffect: Big; - investment: Big; - investmentWithCurrencyEffect: Big; - marketPrice: number; - marketPriceInBaseCurrency: number; - netPerformance: Big; - netPerformancePercentage: Big; - netPerformancePercentageWithCurrencyEffect: Big; - netPerformanceWithCurrencyEffect: Big; - quantity: Big; - symbol: string; - tags?: Tag[]; - timeWeightedInvestment: Big; - timeWeightedInvestmentWithCurrencyEffect: Big; - transactionCount: number; - valueInBaseCurrency: Big; -} diff --git a/libs/common/src/lib/models/index.ts b/libs/common/src/lib/models/index.ts new file mode 100644 index 000000000..0dd601a0e --- /dev/null +++ b/libs/common/src/lib/models/index.ts @@ -0,0 +1,4 @@ +import { PortfolioSnapshot } from './portfolio-snapshot'; +import { TimelinePosition } from './timeline-position'; + +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..909f44f2a --- /dev/null +++ b/libs/common/src/lib/models/portfolio-snapshot.ts @@ -0,0 +1,82 @@ +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 } 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; + + @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; +} diff --git a/libs/common/src/lib/models/timeline-position.ts b/libs/common/src/lib/models/timeline-position.ts new file mode 100644 index 000000000..412449590 --- /dev/null +++ b/libs/common/src/lib/models/timeline-position.ts @@ -0,0 +1,92 @@ +import { transformToBig } from '@ghostfolio/common/class-transformer'; + +import { DataSource, Tag } from '@prisma/client'; +import { Big } from 'big.js'; +import { Transform, Type } from 'class-transformer'; + +export class TimelinePosition { + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + averagePrice: Big; + + currency: string; + dataSource: DataSource; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + dividend: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + dividendInBaseCurrency: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + fee: Big; + + firstBuyDate: string; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + grossPerformance: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + grossPerformancePercentage: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + grossPerformancePercentageWithCurrencyEffect: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + grossPerformanceWithCurrencyEffect: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + investment: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + investmentWithCurrencyEffect: Big; + + marketPrice: number; + marketPriceInBaseCurrency: number; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + netPerformance: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + netPerformancePercentage: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + netPerformancePercentageWithCurrencyEffect: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + netPerformanceWithCurrencyEffect: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + quantity: Big; + + symbol: string; + tags?: Tag[]; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + timeWeightedInvestment: Big; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + timeWeightedInvestmentWithCurrencyEffect: Big; + + transactionCount: number; + + @Transform(transformToBig, { toClassOnly: true }) + @Type(() => Big) + valueInBaseCurrency: Big; +}