From fd5a058b40cb79f5250c789a91897ff7e83544e2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Oct 2025 11:10:13 +0200 Subject: [PATCH 1/8] Set up test --- ...ulator-btceur-in-base-currency-eur.spec.ts | 174 ++++++++++++++++++ .../exchange-rate-data.service.mock.ts | 5 + 2 files changed, 179 insertions(+) create mode 100644 apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts new file mode 100644 index 000000000..29237fde4 --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts @@ -0,0 +1,174 @@ +import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; +import { + activityDummyData, + loadExportFile, + symbolProfileDummyData, + userDummyData +} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; +import { 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 { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; +import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; +import { parseDate } from '@ghostfolio/common/helper'; +import { Export } from '@ghostfolio/common/interfaces'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; + +import { Big } from 'big.js'; +import { join } from 'node:path'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', + () => { + return { + // eslint-disable-next-line @typescript-eslint/naming-convention + PortfolioSnapshotService: jest.fn().mockImplementation(() => { + return PortfolioSnapshotServiceMock; + }) + }; + } +); + +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 exportResponse: Export; + + let configurationService: ConfigurationService; + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let portfolioCalculatorFactory: PortfolioCalculatorFactory; + let portfolioSnapshotService: PortfolioSnapshotService; + let redisCacheService: RedisCacheService; + + beforeAll(() => { + exportResponse = loadExportFile( + join(__dirname, '../../../../../../../test/import/ok/btceur.json') + ); + }); + + beforeEach(() => { + configurationService = new ConfigurationService(); + + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + portfolioSnapshotService = new PortfolioSnapshotService(null); + + redisCacheService = new RedisCacheService(null, null); + + portfolioCalculatorFactory = new PortfolioCalculatorFactory( + configurationService, + currentRateService, + exchangeRateDataService, + portfolioSnapshotService, + redisCacheService + ); + }); + + describe('get current positions', () => { + it.only('with BTCUSD buy (in EUR)', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2022-01-14').getTime()); + + const activities: Activity[] = exportResponse.activities.map( + (activity) => ({ + ...activityDummyData, + ...activity, + date: parseDate(activity.date), + feeInAssetProfileCurrency: 4.46, + SymbolProfile: { + ...symbolProfileDummyData, + currency: 'USD', + dataSource: activity.dataSource, + name: 'Bitcoin', + symbol: activity.symbol + }, + unitPriceInAssetProfileCurrency: 44558.42 + }) + ); + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'CHF', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + expect(portfolioSnapshot).toMatchObject({ + currentValueInBaseCurrency: new Big('43099.7'), + errors: [], + hasErrors: false, + positions: [ + { + averagePrice: new Big('44558.42'), + currency: 'USD', + dataSource: 'YAHOO', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('4.46'), + feeInBaseCurrency: new Big('3.94'), + firstBuyDate: '2021-12-12', + grossPerformance: new Big('-1458.72'), + grossPerformancePercentage: new Big('-0.03273724696701543726'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '-0.03273724696701543726' + ), + grossPerformanceWithCurrencyEffect: new Big('-1458.72'), + investment: new Big('44558.42'), + investmentWithCurrencyEffect: new Big('44558.42'), + netPerformance: new Big('-1463.18'), + netPerformancePercentage: new Big('-0.03283734028271199921'), + netPerformancePercentageWithCurrencyEffectMap: { + max: new Big('-0.03283734028271199921') + }, + netPerformanceWithCurrencyEffectMap: { + max: new Big('-1463.18') + }, + marketPrice: 43099.7, + marketPriceInBaseCurrency: 43099.7, + quantity: new Big('1'), + symbol: 'BTCUSD', + tags: [], + timeWeightedInvestment: new Big('44558.42'), + timeWeightedInvestmentWithCurrencyEffect: new Big('44558.42'), + transactionCount: 1, + valueInBaseCurrency: new Big('43099.7') + } + ], + totalFeesWithCurrencyEffect: new Big('4.46'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('44558.42'), + totalInvestmentWithCurrencyEffect: new Big('44558.42'), + totalLiabilitiesWithCurrencyEffect: new Big('0') + }); + }); + }); +}); diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts index 8f5d1c28a..67ba09d55 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts @@ -17,6 +17,11 @@ export const ExchangeRateDataServiceMock = { '2023-07-10': 0.8854 } }); + } else if (targetCurrency === 'EUR') { + return Promise.resolve({ + EUREUR: {}, + USDEUR: {} + }); } else if (targetCurrency === 'USD') { return Promise.resolve({ USDUSD: { From 9bd724ecafba114ffced166dbf04a482749f5eaa Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Oct 2025 11:11:16 +0200 Subject: [PATCH 2/8] Set up test --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 512f61b6d..fcd33946c 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "test": "npm run test:api && npm run test:common", "test:api": "npx dotenv-cli -e .env.example -- nx test api", "test:common": "npx dotenv-cli -e .env.example -- nx test common", - "test:single": "nx run api:test --test-file object.helper.spec.ts", + "test:single": "nx run api:test --test-file portfolio-calculator-btceur-with-base-currency-eur.spec.ts", "ts-node": "ts-node", "update": "nx migrate latest", "watch:server": "nx run api:copy-assets && nx run api:build --watch", From 3233dc0279d1e4f946d103c8203f33f160e8f630 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 18 Oct 2025 12:16:58 +0200 Subject: [PATCH 3/8] Add ExchangeRateDataServiceMock --- ...calculator-btceur-in-base-currency-eur.spec.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts index 29237fde4..c0044e67c 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts @@ -12,6 +12,7 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.s 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 { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { parseDate } from '@ghostfolio/common/helper'; @@ -23,18 +24,27 @@ import { join } from 'node:path'; jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { return { - // eslint-disable-next-line @typescript-eslint/naming-convention CurrentRateService: jest.fn().mockImplementation(() => { return CurrentRateServiceMock; }) }; }); +jest.mock( + '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', + () => { + return { + ExchangeRateDataService: jest.fn().mockImplementation(() => { + return ExchangeRateDataServiceMock; + }) + }; + } +); + jest.mock( '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', () => { return { - // eslint-disable-next-line @typescript-eslint/naming-convention PortfolioSnapshotService: jest.fn().mockImplementation(() => { return PortfolioSnapshotServiceMock; }) @@ -44,7 +54,6 @@ jest.mock( 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; }) From 0127666a1f97bd029743b259b53a373ae5e55492 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 25 Oct 2025 10:20:02 +0200 Subject: [PATCH 4/8] Set up test --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fcd33946c..04014d5d5 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "test": "npm run test:api && npm run test:common", "test:api": "npx dotenv-cli -e .env.example -- nx test api", "test:common": "npx dotenv-cli -e .env.example -- nx test common", - "test:single": "nx run api:test --test-file portfolio-calculator-btceur-with-base-currency-eur.spec.ts", + "test:single": "nx run api:test --test-file portfolio-calculator-btceur-in-base-currency-eur.spec.ts", "ts-node": "ts-node", "update": "nx migrate latest", "watch:server": "nx run api:copy-assets && nx run api:build --watch", From e2ece2515243e6d5b50b22810f60d99122330093 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 26 Oct 2025 08:15:47 +0100 Subject: [PATCH 5/8] Fix import --- .../portfolio-calculator-btceur-in-base-currency-eur.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts index c0044e67c..78fa86323 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts @@ -16,7 +16,7 @@ import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-r import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { parseDate } from '@ghostfolio/common/helper'; -import { Export } from '@ghostfolio/common/interfaces'; +import { ExportResponse } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Big } from 'big.js'; @@ -61,7 +61,7 @@ jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { }); describe('PortfolioCalculator', () => { - let exportResponse: Export; + let exportResponse: ExportResponse; let configurationService: ConfigurationService; let currentRateService: CurrentRateService; From 3fb00d3a39bf4a80cc5cdd959cc5234d86f3f835 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 26 Oct 2025 08:29:11 +0100 Subject: [PATCH 6/8] Set up test --- ...ulator-btceur-in-base-currency-eur.spec.ts | 51 ++----------------- 1 file changed, 4 insertions(+), 47 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts index 78fa86323..f48d774ae 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts @@ -125,58 +125,15 @@ describe('PortfolioCalculator', () => { const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ activities, calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', + currency: 'EUR', userId: userDummyData.id }); const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); - expect(portfolioSnapshot).toMatchObject({ - currentValueInBaseCurrency: new Big('43099.7'), - errors: [], - hasErrors: false, - positions: [ - { - averagePrice: new Big('44558.42'), - currency: 'USD', - dataSource: 'YAHOO', - dividend: new Big('0'), - dividendInBaseCurrency: new Big('0'), - fee: new Big('4.46'), - feeInBaseCurrency: new Big('3.94'), - firstBuyDate: '2021-12-12', - grossPerformance: new Big('-1458.72'), - grossPerformancePercentage: new Big('-0.03273724696701543726'), - grossPerformancePercentageWithCurrencyEffect: new Big( - '-0.03273724696701543726' - ), - grossPerformanceWithCurrencyEffect: new Big('-1458.72'), - investment: new Big('44558.42'), - investmentWithCurrencyEffect: new Big('44558.42'), - netPerformance: new Big('-1463.18'), - netPerformancePercentage: new Big('-0.03283734028271199921'), - netPerformancePercentageWithCurrencyEffectMap: { - max: new Big('-0.03283734028271199921') - }, - netPerformanceWithCurrencyEffectMap: { - max: new Big('-1463.18') - }, - marketPrice: 43099.7, - marketPriceInBaseCurrency: 43099.7, - quantity: new Big('1'), - symbol: 'BTCUSD', - tags: [], - timeWeightedInvestment: new Big('44558.42'), - timeWeightedInvestmentWithCurrencyEffect: new Big('44558.42'), - transactionCount: 1, - valueInBaseCurrency: new Big('43099.7') - } - ], - totalFeesWithCurrencyEffect: new Big('4.46'), - totalInterestWithCurrencyEffect: new Big('0'), - totalInvestment: new Big('44558.42'), - totalInvestmentWithCurrencyEffect: new Big('44558.42'), - totalLiabilitiesWithCurrencyEffect: new Big('0') + expect(portfolioSnapshot.positions[0]).toMatchObject({ + fee: new Big('4.46'), + feeInBaseCurrency: new Big('3.94') }); }); }); From 9d1cc8a3f7487d479b5775b44da32567fc96dc34 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 26 Oct 2025 08:31:51 +0100 Subject: [PATCH 7/8] Set up test --- ...olio-calculator-btceur-in-base-currency-eur.spec.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts index f48d774ae..fd9c75fe7 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts @@ -131,9 +131,13 @@ describe('PortfolioCalculator', () => { const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); - expect(portfolioSnapshot.positions[0]).toMatchObject({ - fee: new Big('4.46'), - feeInBaseCurrency: new Big('3.94') + expect(portfolioSnapshot).toMatchObject({ + positions: [ + { + fee: new Big('4.46'), + feeInBaseCurrency: new Big('3.94') + } + ] }); }); }); From 0a677b5c768447080994ac4af2375bdceb1662de Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 27 Oct 2025 21:12:28 +0100 Subject: [PATCH 8/8] Set up test --- ...io-calculator-btceur-in-base-currency-eur.spec.ts | 12 ++++-------- .../exchange-rate-data.service.mock.ts | 9 +++++++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts index fd9c75fe7..87893e647 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur-in-base-currency-eur.spec.ts @@ -131,14 +131,10 @@ describe('PortfolioCalculator', () => { const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); - expect(portfolioSnapshot).toMatchObject({ - positions: [ - { - fee: new Big('4.46'), - feeInBaseCurrency: new Big('3.94') - } - ] - }); + expect(portfolioSnapshot.positions[0].fee).toEqual(new Big(4.46)); + expect( + portfolioSnapshot.positions[0].feeInBaseCurrency.toNumber() + ).toBeCloseTo(3.94, 1); }); }); }); diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts index 67ba09d55..076375523 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.mock.ts @@ -19,14 +19,19 @@ export const ExchangeRateDataServiceMock = { }); } else if (targetCurrency === 'EUR') { return Promise.resolve({ - EUREUR: {}, - USDEUR: {} + EUREUR: { + '2021-12-12': 1 + }, + USDEUR: { + '2021-12-12': 0.8855 + } }); } else if (targetCurrency === 'USD') { return Promise.resolve({ USDUSD: { '2018-01-01': 1, '2021-11-16': 1, + '2021-12-12': 1, '2023-07-10': 1 } });