diff --git a/CHANGELOG.md b/CHANGELOG.md index be76df609..f60446fcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added OIDC (OpenID Connect) as a login auth provider +### Changed + +- Eliminated `uuid` in favor of using `randomUUID` from `node:crypto` + +### Fixed + +- Fixed an issue with the exchange rate calculation when converting between derived currencies and their root currencies + ## 2.219.0 - 2025-11-23 ### Added diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index a5f3dda96..2deef1c44 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -35,7 +35,7 @@ import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; import { endOfToday, isAfter, isSameSecond, parseISO } from 'date-fns'; import { omit, uniqBy } from 'lodash'; -import { v4 as uuidv4 } from 'uuid'; +import { randomUUID } from 'node:crypto'; import { ImportDataDto } from './import-data.dto'; @@ -277,7 +277,7 @@ export class ImportService { // Asset profile belongs to a different user if (existingAssetProfile) { - const symbol = uuidv4(); + const symbol = randomUUID(); assetProfileSymbolMapping[assetProfile.symbol] = symbol; assetProfile.symbol = symbol; } @@ -496,7 +496,7 @@ export class ImportService { accountId: validatedAccount?.id, accountUserId: undefined, createdAt: new Date(), - id: uuidv4(), + id: randomUUID(), isDraft: isAfter(date, endOfToday()), SymbolProfile: { assetClass, diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 7dc6c646d..001d43b7a 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -37,7 +37,7 @@ import { Big } from 'big.js'; import { isUUID } from 'class-validator'; import { endOfToday, isAfter } from 'date-fns'; import { groupBy, uniqBy } from 'lodash'; -import { v4 as uuidv4 } from 'uuid'; +import { randomUUID } from 'node:crypto'; @Injectable() export class OrderService { @@ -143,7 +143,7 @@ export class OrderService { } else { // Create custom asset profile name = name ?? data.SymbolProfile.connectOrCreate.create.symbol; - symbol = uuidv4(); + symbol = randomUUID(); } data.SymbolProfile.connectOrCreate.create.assetClass = assetClass; diff --git a/apps/api/src/services/demo/demo.service.ts b/apps/api/src/services/demo/demo.service.ts index 8f3658736..a24716d96 100644 --- a/apps/api/src/services/demo/demo.service.ts +++ b/apps/api/src/services/demo/demo.service.ts @@ -7,7 +7,7 @@ import { } from '@ghostfolio/common/config'; import { Injectable } from '@nestjs/common'; -import { v4 as uuidv4 } from 'uuid'; +import { randomUUID } from 'node:crypto'; @Injectable() export class DemoService { @@ -41,7 +41,7 @@ export class DemoService { accountId: demoAccountId, accountUserId: demoUserId, comment: null, - id: uuidv4(), + id: randomUUID(), userId: demoUserId }; }); 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 47c67c3de..8c1ba5b41 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 @@ -30,6 +30,7 @@ import ms from 'ms'; export class ExchangeRateDataService { private currencies: string[] = []; private currencyPairs: DataGatheringItem[] = []; + private derivedCurrencyFactors: { [currencyPair: string]: number } = {}; private exchangeRates: { [currencyPair: string]: number } = {}; public constructor( @@ -135,8 +136,14 @@ export class ExchangeRateDataService { public async initialize() { this.currencies = await this.prepareCurrencies(); this.currencyPairs = []; + this.derivedCurrencyFactors = {}; this.exchangeRates = {}; + for (const { currency, factor, rootCurrency } of DERIVED_CURRENCIES) { + this.derivedCurrencyFactors[`${currency}${rootCurrency}`] = 1 / factor; + this.derivedCurrencyFactors[`${rootCurrency}${currency}`] = factor; + } + for (const { currency1, currency2, @@ -266,10 +273,14 @@ export class ExchangeRateDataService { return this.toCurrency(aValue, aFromCurrency, aToCurrency); } + const derivedCurrencyFactor = + this.derivedCurrencyFactors[`${aFromCurrency}${aToCurrency}`]; let factor: number; if (aFromCurrency === aToCurrency) { factor = 1; + } else if (derivedCurrencyFactor) { + factor = derivedCurrencyFactor; } else { const dataSource = this.dataProviderService.getDataSourceForExchangeRates(); @@ -357,111 +368,120 @@ export class ExchangeRateDataService { for (const date of dates) { factors[format(date, DATE_FORMAT)] = 1; } - } else { - const dataSource = - this.dataProviderService.getDataSourceForExchangeRates(); - const symbol = `${currencyFrom}${currencyTo}`; - const marketData = await this.marketDataService.getRange({ - assetProfileIdentifiers: [ - { - dataSource, - symbol - } - ], - dateQuery: { gte: startDate, lt: endDate } - }); + return factors; + } + + const derivedCurrencyFactor = + this.derivedCurrencyFactors[`${currencyFrom}${currencyTo}`]; + + if (derivedCurrencyFactor) { + for (const date of dates) { + factors[format(date, DATE_FORMAT)] = derivedCurrencyFactor; + } - if (marketData?.length > 0) { - for (const { date, marketPrice } of marketData) { - factors[format(date, DATE_FORMAT)] = marketPrice; + return factors; + } + + const dataSource = this.dataProviderService.getDataSourceForExchangeRates(); + const symbol = `${currencyFrom}${currencyTo}`; + + const marketData = await this.marketDataService.getRange({ + assetProfileIdentifiers: [ + { + dataSource, + symbol } - } else { - // Calculate indirectly via base currency + ], + dateQuery: { gte: startDate, lt: endDate } + }); - const marketPriceBaseCurrencyFromCurrency: { - [dateString: string]: number; - } = {}; - const marketPriceBaseCurrencyToCurrency: { - [dateString: string]: number; - } = {}; + if (marketData?.length > 0) { + for (const { date, marketPrice } of marketData) { + factors[format(date, DATE_FORMAT)] = marketPrice; + } + } else { + // Calculate indirectly via base currency + + const marketPriceBaseCurrencyFromCurrency: { + [dateString: string]: number; + } = {}; + const marketPriceBaseCurrencyToCurrency: { + [dateString: string]: number; + } = {}; + + try { + if (currencyFrom === DEFAULT_CURRENCY) { + for (const date of dates) { + marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = 1; + } + } else { + const marketData = await this.marketDataService.getRange({ + assetProfileIdentifiers: [ + { + dataSource, + symbol: `${DEFAULT_CURRENCY}${currencyFrom}` + } + ], + dateQuery: { gte: startDate, lt: endDate } + }); - try { - if (currencyFrom === DEFAULT_CURRENCY) { - for (const date of dates) { - marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = - 1; - } - } else { - const marketData = await this.marketDataService.getRange({ - assetProfileIdentifiers: [ - { - dataSource, - symbol: `${DEFAULT_CURRENCY}${currencyFrom}` - } - ], - dateQuery: { gte: startDate, lt: endDate } - }); - - for (const { date, marketPrice } of marketData) { - marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = - marketPrice; - } + for (const { date, marketPrice } of marketData) { + marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)] = + marketPrice; } - } catch {} + } + } catch {} - try { - if (currencyTo === DEFAULT_CURRENCY) { - for (const date of dates) { - marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = 1; - } - } else { - const marketData = await this.marketDataService.getRange({ - assetProfileIdentifiers: [ - { - dataSource, - symbol: `${DEFAULT_CURRENCY}${currencyTo}` - } - ], - dateQuery: { - gte: startDate, - lt: endDate + try { + if (currencyTo === DEFAULT_CURRENCY) { + for (const date of dates) { + marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = 1; + } + } else { + const marketData = await this.marketDataService.getRange({ + assetProfileIdentifiers: [ + { + dataSource, + symbol: `${DEFAULT_CURRENCY}${currencyTo}` } - }); - - for (const { date, marketPrice } of marketData) { - marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = - marketPrice; + ], + dateQuery: { + gte: startDate, + lt: endDate } + }); + + for (const { date, marketPrice } of marketData) { + marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)] = + marketPrice; } - } catch {} + } + } catch {} - for (const date of dates) { - try { - const factor = - (1 / - marketPriceBaseCurrencyFromCurrency[ - format(date, DATE_FORMAT) - ]) * - marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)]; - - if (isNaN(factor)) { - throw new Error('Exchange rate is not a number'); - } else { - factors[format(date, DATE_FORMAT)] = factor; - } - } catch { - let errorMessage = `No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format( - date, - DATE_FORMAT - )}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom}`; - - if (DEFAULT_CURRENCY !== currencyTo) { - errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`; - } + for (const date of dates) { + try { + const factor = + (1 / + marketPriceBaseCurrencyFromCurrency[format(date, DATE_FORMAT)]) * + marketPriceBaseCurrencyToCurrency[format(date, DATE_FORMAT)]; - Logger.error(`${errorMessage}.`, 'ExchangeRateDataService'); + if (isNaN(factor)) { + throw new Error('Exchange rate is not a number'); + } else { + factors[format(date, DATE_FORMAT)] = factor; } + } catch { + let errorMessage = `No exchange rate has been found for ${currencyFrom}${currencyTo} at ${format( + date, + DATE_FORMAT + )}. Please complement market data for ${DEFAULT_CURRENCY}${currencyFrom}`; + + if (DEFAULT_CURRENCY !== currencyTo) { + errorMessage = `${errorMessage} and ${DEFAULT_CURRENCY}${currencyTo}`; + } + + Logger.error(`${errorMessage}.`, 'ExchangeRateDataService'); } } } diff --git a/package-lock.json b/package-lock.json index 0c65be3b8..ac64925a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,7 +90,6 @@ "svgmap": "2.14.0", "tablemark": "4.1.0", "twitter-api-v2": "1.27.0", - "uuid": "11.1.0", "yahoo-finance2": "3.10.2", "zone.js": "0.15.1" }, @@ -40639,6 +40638,7 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", + "optional": true, "bin": { "uuid": "dist/esm/bin/uuid" } diff --git a/package.json b/package.json index 379f61cb6..d10f81a73 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,6 @@ "svgmap": "2.14.0", "tablemark": "4.1.0", "twitter-api-v2": "1.27.0", - "uuid": "11.1.0", "yahoo-finance2": "3.10.2", "zone.js": "0.15.1" },