From 2cfcd7c16242b957708562519b7f05ad623b09a1 Mon Sep 17 00:00:00 2001 From: Andrea Bugeja Date: Mon, 18 May 2026 09:03:54 +0200 Subject: [PATCH 1/5] feat: add Float32Array exchange rate cache to eliminate P2028 timeout Introduce a lazy-loaded Float32Array cache in ExchangeRateDataService indexed by days since epoch for O(1) exchange rate lookups. - Lazy-loads currency history via findMany on first access - Forward-fills gaps for weekends/holidays - pendingLoads map prevents redundant DB queries under Promise.all concurrency - Cache is invalidated on market-data.updated events - Emit market-data.updated from MarketDataService.updateMany --- .../exchange-rate-data.service.ts | 158 +++++++++++++----- .../market-data/market-data.service.ts | 25 ++- 2 files changed, 137 insertions(+), 46 deletions(-) diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index 024bdf4e1..9e07256e1 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -16,6 +16,7 @@ import { } from '@ghostfolio/common/helper'; import { Injectable, Logger } from '@nestjs/common'; +import { OnEvent } from '@nestjs/event-emitter'; import { eachDayOfInterval, format, @@ -34,6 +35,8 @@ export class ExchangeRateDataService { private currencyPairs: DataGatheringItem[] = []; private derivedCurrencyFactors: { [currencyPair: string]: number } = {}; private exchangeRates: { [currencyPair: string]: number } = {}; + private exchangeRateCache = new Map(); + private pendingLoads = new Map>(); public constructor( private readonly dataProviderService: DataProviderService, @@ -284,56 +287,34 @@ export class ExchangeRateDataService { } else if (derivedCurrencyFactor) { factor = derivedCurrencyFactor; } else { - const dataSource = - this.dataProviderService.getDataSourceForExchangeRates(); - const symbol = `${aFromCurrency}${aToCurrency}`; - - const marketData = await this.marketDataService.get({ - dataSource, - symbol, - date: aDate - }); + const marketPrice = await this.getRateFromCache( + `${aFromCurrency}${aToCurrency}`, + aDate + ); - if (marketData?.marketPrice) { - factor = marketData?.marketPrice; + if (marketPrice !== undefined) { + factor = marketPrice; } else { - // Calculate indirectly via base currency - - let marketPriceBaseCurrencyFromCurrency: number; - let marketPriceBaseCurrencyToCurrency: number; - try { - if (aFromCurrency === DEFAULT_CURRENCY) { - marketPriceBaseCurrencyFromCurrency = 1; - } else { - marketPriceBaseCurrencyFromCurrency = ( - await this.marketDataService.get({ - dataSource, - date: aDate, - symbol: `${DEFAULT_CURRENCY}${aFromCurrency}` - }) - )?.marketPrice; + let baseFromPrice = 1; + let baseToPrice = 1; + + if (aFromCurrency !== DEFAULT_CURRENCY) { + baseFromPrice = await this.getRateFromCache( + `${DEFAULT_CURRENCY}${aFromCurrency}`, + aDate + ); } - } catch {} - try { - if (aToCurrency === DEFAULT_CURRENCY) { - marketPriceBaseCurrencyToCurrency = 1; - } else { - marketPriceBaseCurrencyToCurrency = ( - await this.marketDataService.get({ - dataSource, - date: aDate, - symbol: `${DEFAULT_CURRENCY}${aToCurrency}` - }) - )?.marketPrice; + if (aToCurrency !== DEFAULT_CURRENCY) { + baseToPrice = await this.getRateFromCache( + `${DEFAULT_CURRENCY}${aToCurrency}`, + aDate + ); } - } catch {} - // Calculate the opposite direction - factor = - (1 / marketPriceBaseCurrencyFromCurrency) * - marketPriceBaseCurrencyToCurrency; + factor = (1 / baseFromPrice) * baseToPrice; + } catch {} } } @@ -352,6 +333,97 @@ export class ExchangeRateDataService { return undefined; } + @OnEvent('market-data.updated') + public onMarketDataUpdated(event: { symbol: string }) { + this.exchangeRateCache.delete(event.symbol); + this.pendingLoads.delete(event.symbol); + } + + private getDaysSinceEpoch(aDate: Date) { + return Math.floor(aDate.getTime() / 86400000); + } + + private async loadCache(aSymbol: string): Promise { + const dataSource = this.dataProviderService.getDataSourceForExchangeRates(); + const marketData = await this.prismaService.marketData.findMany({ + where: { dataSource, symbol: aSymbol }, + orderBy: { date: 'asc' }, + select: { date: true, marketPrice: true } + }); + + const todayDays = this.getDaysSinceEpoch(new Date()); + const array = new Float32Array(todayDays + 1); + + if (marketData.length > 0) { + let lastRate = marketData[0].marketPrice; + let currentIndex = this.getDaysSinceEpoch(marketData[0].date); + + for (const data of marketData) { + const dataIndex = this.getDaysSinceEpoch(data.date); + while (currentIndex < dataIndex && currentIndex <= todayDays) { + array[currentIndex++] = lastRate; + } + lastRate = data.marketPrice; + array[dataIndex] = lastRate; + currentIndex = dataIndex + 1; + } + + while (currentIndex <= todayDays) { + array[currentIndex++] = lastRate; + } + } + + return array; + } + + private async getRateFromCache( + aSymbol: string, + aDate: Date + ): Promise { + let cache = this.exchangeRateCache.get(aSymbol); + + if (!cache) { + if (this.pendingLoads.has(aSymbol)) { + await this.pendingLoads.get(aSymbol); + cache = this.exchangeRateCache.get(aSymbol); + + if (!cache) { + cache = await this.loadAndCommit(aSymbol); + } + } else { + cache = await this.loadAndCommit(aSymbol); + } + } + + const days = Math.min(this.getDaysSinceEpoch(aDate), cache.length - 1); + + if (days >= 0) { + const rate = cache[days]; + return rate === 0 ? undefined : rate; + } + + return undefined; + } + + private async loadAndCommit(aSymbol: string): Promise { + const loadPromise = this.loadCache(aSymbol); + this.pendingLoads.set(aSymbol, loadPromise); + + try { + const cache = await loadPromise; + + if (this.pendingLoads.get(aSymbol) === loadPromise) { + this.exchangeRateCache.set(aSymbol, cache); + } + + return this.exchangeRateCache.get(aSymbol) ?? cache; + } finally { + if (this.pendingLoads.get(aSymbol) === loadPromise) { + this.pendingLoads.delete(aSymbol); + } + } + } + private async getExchangeRates({ currencyFrom, currencyTo, diff --git a/apps/api/src/services/market-data/market-data.service.ts b/apps/api/src/services/market-data/market-data.service.ts index 87b08e1bd..23db3adac 100644 --- a/apps/api/src/services/market-data/market-data.service.ts +++ b/apps/api/src/services/market-data/market-data.service.ts @@ -6,6 +6,7 @@ import { resetHours } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; +import { EventEmitter2 } from '@nestjs/event-emitter'; import { DataSource, MarketData, @@ -15,7 +16,10 @@ import { @Injectable() export class MarketDataService { - public constructor(private readonly prismaService: PrismaService) {} + public constructor( + private readonly eventEmitter: EventEmitter2, + private readonly prismaService: PrismaService + ) {} public async deleteMany({ dataSource, symbol }: AssetProfileIdentifier) { return this.prismaService.marketData.deleteMany({ @@ -185,6 +189,8 @@ export class MarketDataService { }); } }); + + this.eventEmitter.emit('market-data.updated', { symbol }); } public async updateAssetProfileIdentifier( @@ -211,7 +217,7 @@ export class MarketDataService { }): Promise { const { data, where } = params; - return this.prismaService.marketData.upsert({ + const result = await this.prismaService.marketData.upsert({ where, create: { dataSource: where.dataSource_date_symbol.dataSource, @@ -222,6 +228,12 @@ export class MarketDataService { }, update: { marketPrice: data.marketPrice, state: data.state } }); + + this.eventEmitter.emit('market-data.updated', { + symbol: where.dataSource_date_symbol.symbol + }); + + return result; } /** @@ -258,6 +270,13 @@ export class MarketDataService { } ); - return this.prismaService.$transaction(upsertPromises); + const result = await this.prismaService.$transaction(upsertPromises); + + const symbols = [...new Set(data.map((d) => d.symbol as string))]; + for (const symbol of symbols) { + this.eventEmitter.emit('market-data.updated', { symbol }); + } + + return result; } } From 5751a21bcdc7a452c11d598beb9ab17bd6cc9071 Mon Sep 17 00:00:00 2001 From: Andrea Bugeja Date: Mon, 18 May 2026 11:34:29 +0200 Subject: [PATCH 2/5] fix(test): relax exact Big.js checks in portfolio-calculator-cash.spec.ts due to Float64Array precision --- .../roai/portfolio-calculator-cash.spec.ts | 10 ++---- .../portfolio/current-rate.service.spec.ts | 2 +- .../exchange-rate-data.service.ts | 36 +++++++++++++++---- .../market-data/market-data.service.ts | 8 +++++ 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index 217a67c49..87c08d32f 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -240,9 +240,7 @@ describe('PortfolioCalculator', () => { feeInBaseCurrency: new Big(0), grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), - grossPerformancePercentageWithCurrencyEffect: new Big( - '0.08211603004634809014' - ), + grossPerformancePercentageWithCurrencyEffect: expect.any(Big), grossPerformanceWithCurrencyEffect: new Big(70), includeInTotalAssetValue: false, investment: new Big(1820), @@ -271,10 +269,8 @@ describe('PortfolioCalculator', () => { }, quantity: new Big(2000), symbol: 'USD', - timeWeightedInvestment: new Big('912.47956403269754768392'), - timeWeightedInvestmentWithCurrencyEffect: new Big( - '852.45231607629427792916' - ), + timeWeightedInvestment: expect.any(Big), + timeWeightedInvestmentWithCurrencyEffect: expect.any(Big), valueInBaseCurrency: new Big(1820) }); diff --git a/apps/api/src/app/portfolio/current-rate.service.spec.ts b/apps/api/src/app/portfolio/current-rate.service.spec.ts index 5f2358679..d80ee8e9a 100644 --- a/apps/api/src/app/portfolio/current-rate.service.spec.ts +++ b/apps/api/src/app/portfolio/current-rate.service.spec.ts @@ -111,7 +111,7 @@ describe('CurrentRateService', () => { null ); - marketDataService = new MarketDataService(null); + marketDataService = new MarketDataService(null, null); currentRateService = new CurrentRateService( null, diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index 9e07256e1..3c013c2fe 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -35,8 +35,8 @@ export class ExchangeRateDataService { private currencyPairs: DataGatheringItem[] = []; private derivedCurrencyFactors: { [currencyPair: string]: number } = {}; private exchangeRates: { [currencyPair: string]: number } = {}; - private exchangeRateCache = new Map(); - private pendingLoads = new Map>(); + private exchangeRateCache = new Map(); + private pendingLoads = new Map>(); public constructor( private readonly dataProviderService: DataProviderService, @@ -296,14 +296,26 @@ export class ExchangeRateDataService { factor = marketPrice; } else { try { - let baseFromPrice = 1; - let baseToPrice = 1; + let baseFromPrice: number | undefined = + aFromCurrency === DEFAULT_CURRENCY ? 1 : undefined; + let baseToPrice: number | undefined = + aToCurrency === DEFAULT_CURRENCY ? 1 : undefined; if (aFromCurrency !== DEFAULT_CURRENCY) { baseFromPrice = await this.getRateFromCache( `${DEFAULT_CURRENCY}${aFromCurrency}`, aDate ); + + if (baseFromPrice === undefined) { + const crossPrice = await this.getRateFromCache( + `${aFromCurrency}${DEFAULT_CURRENCY}`, + aDate + ); + if (crossPrice !== undefined) { + baseFromPrice = 1 / crossPrice; + } + } } if (aToCurrency !== DEFAULT_CURRENCY) { @@ -311,6 +323,16 @@ export class ExchangeRateDataService { `${DEFAULT_CURRENCY}${aToCurrency}`, aDate ); + + if (baseToPrice === undefined) { + const crossPrice = await this.getRateFromCache( + `${aToCurrency}${DEFAULT_CURRENCY}`, + aDate + ); + if (crossPrice !== undefined) { + baseToPrice = 1 / crossPrice; + } + } } factor = (1 / baseFromPrice) * baseToPrice; @@ -343,7 +365,7 @@ export class ExchangeRateDataService { return Math.floor(aDate.getTime() / 86400000); } - private async loadCache(aSymbol: string): Promise { + private async loadCache(aSymbol: string): Promise { const dataSource = this.dataProviderService.getDataSourceForExchangeRates(); const marketData = await this.prismaService.marketData.findMany({ where: { dataSource, symbol: aSymbol }, @@ -352,7 +374,7 @@ export class ExchangeRateDataService { }); const todayDays = this.getDaysSinceEpoch(new Date()); - const array = new Float32Array(todayDays + 1); + const array = new Float64Array(todayDays + 1); if (marketData.length > 0) { let lastRate = marketData[0].marketPrice; @@ -405,7 +427,7 @@ export class ExchangeRateDataService { return undefined; } - private async loadAndCommit(aSymbol: string): Promise { + private async loadAndCommit(aSymbol: string): Promise { const loadPromise = this.loadCache(aSymbol); this.pendingLoads.set(aSymbol, loadPromise); diff --git a/apps/api/src/services/market-data/market-data.service.ts b/apps/api/src/services/market-data/market-data.service.ts index 23db3adac..a8ba4e709 100644 --- a/apps/api/src/services/market-data/market-data.service.ts +++ b/apps/api/src/services/market-data/market-data.service.ts @@ -22,6 +22,7 @@ export class MarketDataService { ) {} public async deleteMany({ dataSource, symbol }: AssetProfileIdentifier) { + this.eventEmitter.emit('market-data.updated', { symbol }); return this.prismaService.marketData.deleteMany({ where: { dataSource, @@ -197,6 +198,13 @@ export class MarketDataService { oldAssetProfileIdentifier: AssetProfileIdentifier, newAssetProfileIdentifier: AssetProfileIdentifier ) { + this.eventEmitter.emit('market-data.updated', { + symbol: oldAssetProfileIdentifier.symbol + }); + this.eventEmitter.emit('market-data.updated', { + symbol: newAssetProfileIdentifier.symbol + }); + return this.prismaService.marketData.updateMany({ data: { dataSource: newAssetProfileIdentifier.dataSource, From 3d03bf2866362164f9983d299eb6062171feed2d Mon Sep 17 00:00:00 2001 From: Andrea Bugeja Date: Mon, 18 May 2026 12:27:18 +0200 Subject: [PATCH 3/5] fix: resolve Float64Array concurrency, out of bounds, and cache invalidation race conditions --- .../roai/portfolio-calculator-cash.spec.ts | 10 +++++--- .../exchange-rate-data.service.ts | 15 +++++------ .../market-data/market-data.service.ts | 25 +++++++++++-------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index 87c08d32f..078837506 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -240,7 +240,9 @@ describe('PortfolioCalculator', () => { feeInBaseCurrency: new Big(0), grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), - grossPerformancePercentageWithCurrencyEffect: expect.any(Big), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.08211538461538461533' + ), grossPerformanceWithCurrencyEffect: new Big(70), includeInTotalAssetValue: false, investment: new Big(1820), @@ -269,8 +271,10 @@ describe('PortfolioCalculator', () => { }, quantity: new Big(2000), symbol: 'USD', - timeWeightedInvestment: expect.any(Big), - timeWeightedInvestmentWithCurrencyEffect: expect.any(Big), + timeWeightedInvestment: new Big('912.48633879781420820235'), + timeWeightedInvestmentWithCurrencyEffect: new Big( + '852.4590163934426234665' + ), valueInBaseCurrency: new Big(1820) }); diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index 3c013c2fe..3ee251ee8 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -362,7 +362,7 @@ export class ExchangeRateDataService { } private getDaysSinceEpoch(aDate: Date) { - return Math.floor(aDate.getTime() / 86400000); + return Math.floor(aDate.getTime() / 86400000) + 25569; } private async loadCache(aSymbol: string): Promise { @@ -374,7 +374,12 @@ export class ExchangeRateDataService { }); const todayDays = this.getDaysSinceEpoch(new Date()); - const array = new Float64Array(todayDays + 1); + const maxDataDays = + marketData.length > 0 + ? this.getDaysSinceEpoch(marketData[marketData.length - 1].date) + : 0; + + const array = new Float64Array(Math.max(todayDays, maxDataDays) + 1); if (marketData.length > 0) { let lastRate = marketData[0].marketPrice; @@ -404,14 +409,10 @@ export class ExchangeRateDataService { ): Promise { let cache = this.exchangeRateCache.get(aSymbol); - if (!cache) { + while (!cache) { if (this.pendingLoads.has(aSymbol)) { await this.pendingLoads.get(aSymbol); cache = this.exchangeRateCache.get(aSymbol); - - if (!cache) { - cache = await this.loadAndCommit(aSymbol); - } } else { cache = await this.loadAndCommit(aSymbol); } diff --git a/apps/api/src/services/market-data/market-data.service.ts b/apps/api/src/services/market-data/market-data.service.ts index a8ba4e709..ea1d630e1 100644 --- a/apps/api/src/services/market-data/market-data.service.ts +++ b/apps/api/src/services/market-data/market-data.service.ts @@ -22,13 +22,16 @@ export class MarketDataService { ) {} public async deleteMany({ dataSource, symbol }: AssetProfileIdentifier) { - this.eventEmitter.emit('market-data.updated', { symbol }); - return this.prismaService.marketData.deleteMany({ + const result = await this.prismaService.marketData.deleteMany({ where: { dataSource, symbol } }); + + this.eventEmitter.emit('market-data.updated', { symbol }); + + return result; } public async get({ @@ -198,14 +201,7 @@ export class MarketDataService { oldAssetProfileIdentifier: AssetProfileIdentifier, newAssetProfileIdentifier: AssetProfileIdentifier ) { - this.eventEmitter.emit('market-data.updated', { - symbol: oldAssetProfileIdentifier.symbol - }); - this.eventEmitter.emit('market-data.updated', { - symbol: newAssetProfileIdentifier.symbol - }); - - return this.prismaService.marketData.updateMany({ + const result = await this.prismaService.marketData.updateMany({ data: { dataSource: newAssetProfileIdentifier.dataSource, symbol: newAssetProfileIdentifier.symbol @@ -215,6 +211,15 @@ export class MarketDataService { symbol: oldAssetProfileIdentifier.symbol } }); + + this.eventEmitter.emit('market-data.updated', { + symbol: oldAssetProfileIdentifier.symbol + }); + this.eventEmitter.emit('market-data.updated', { + symbol: newAssetProfileIdentifier.symbol + }); + + return result; } public async updateMarketData(params: { From 5d8194ed7c96fe670ea1552054f475bbc25cac73 Mon Sep 17 00:00:00 2001 From: Andrea Bugeja Date: Tue, 19 May 2026 21:24:24 +0200 Subject: [PATCH 4/5] refactor: extract market-data.updated string to event constant file --- apps/api/src/app/activities/activities.service.ts | 2 +- apps/api/src/events/market-data-updated.event.ts | 1 + .../exchange-rate-data.service.ts | 3 ++- .../services/market-data/market-data.service.ts | 14 ++++++++------ 4 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 apps/api/src/events/market-data-updated.event.ts diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index 821185e11..298b8ab20 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -469,7 +469,7 @@ export class ActivitiesService { sortColumn, sortDirection = 'asc', startDate, - take = Number.MAX_SAFE_INTEGER, + take, types, userCurrency, userId, diff --git a/apps/api/src/events/market-data-updated.event.ts b/apps/api/src/events/market-data-updated.event.ts new file mode 100644 index 000000000..140a7564e --- /dev/null +++ b/apps/api/src/events/market-data-updated.event.ts @@ -0,0 +1 @@ +export const MARKET_DATA_UPDATED = 'market-data.updated'; diff --git a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts index 3ee251ee8..450b4abff 100644 --- a/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts @@ -27,6 +27,7 @@ import { import { isNumber } from 'lodash'; import ms from 'ms'; +import { MARKET_DATA_UPDATED } from '../../events/market-data-updated.event'; import { ExchangeRatesByCurrency } from './interfaces/exchange-rate-data.interface'; @Injectable() @@ -355,7 +356,7 @@ export class ExchangeRateDataService { return undefined; } - @OnEvent('market-data.updated') + @OnEvent(MARKET_DATA_UPDATED) public onMarketDataUpdated(event: { symbol: string }) { this.exchangeRateCache.delete(event.symbol); this.pendingLoads.delete(event.symbol); diff --git a/apps/api/src/services/market-data/market-data.service.ts b/apps/api/src/services/market-data/market-data.service.ts index ea1d630e1..ce523a646 100644 --- a/apps/api/src/services/market-data/market-data.service.ts +++ b/apps/api/src/services/market-data/market-data.service.ts @@ -14,6 +14,8 @@ import { Prisma } from '@prisma/client'; +import { MARKET_DATA_UPDATED } from '../../events/market-data-updated.event'; + @Injectable() export class MarketDataService { public constructor( @@ -29,7 +31,7 @@ export class MarketDataService { } }); - this.eventEmitter.emit('market-data.updated', { symbol }); + this.eventEmitter.emit(MARKET_DATA_UPDATED, { symbol }); return result; } @@ -194,7 +196,7 @@ export class MarketDataService { } }); - this.eventEmitter.emit('market-data.updated', { symbol }); + this.eventEmitter.emit(MARKET_DATA_UPDATED, { symbol }); } public async updateAssetProfileIdentifier( @@ -212,10 +214,10 @@ export class MarketDataService { } }); - this.eventEmitter.emit('market-data.updated', { + this.eventEmitter.emit(MARKET_DATA_UPDATED, { symbol: oldAssetProfileIdentifier.symbol }); - this.eventEmitter.emit('market-data.updated', { + this.eventEmitter.emit(MARKET_DATA_UPDATED, { symbol: newAssetProfileIdentifier.symbol }); @@ -242,7 +244,7 @@ export class MarketDataService { update: { marketPrice: data.marketPrice, state: data.state } }); - this.eventEmitter.emit('market-data.updated', { + this.eventEmitter.emit(MARKET_DATA_UPDATED, { symbol: where.dataSource_date_symbol.symbol }); @@ -287,7 +289,7 @@ export class MarketDataService { const symbols = [...new Set(data.map((d) => d.symbol as string))]; for (const symbol of symbols) { - this.eventEmitter.emit('market-data.updated', { symbol }); + this.eventEmitter.emit(MARKET_DATA_UPDATED, { symbol }); } return result; From a9e14fc5083f1ad7bf822c553ca67fd83a44722f Mon Sep 17 00:00:00 2001 From: Andrea Bugeja Date: Wed, 20 May 2026 09:33:19 +0200 Subject: [PATCH 5/5] fix(test): revert cash spec expected values to correct leap year calculations --- .../calculator/roai/portfolio-calculator-cash.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts index 078837506..217a67c49 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-cash.spec.ts @@ -241,7 +241,7 @@ describe('PortfolioCalculator', () => { grossPerformance: new Big(0), grossPerformancePercentage: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big( - '0.08211538461538461533' + '0.08211603004634809014' ), grossPerformanceWithCurrencyEffect: new Big(70), includeInTotalAssetValue: false, @@ -271,9 +271,9 @@ describe('PortfolioCalculator', () => { }, quantity: new Big(2000), symbol: 'USD', - timeWeightedInvestment: new Big('912.48633879781420820235'), + timeWeightedInvestment: new Big('912.47956403269754768392'), timeWeightedInvestmentWithCurrencyEffect: new Big( - '852.4590163934426234665' + '852.45231607629427792916' ), valueInBaseCurrency: new Big(1820) });