From 5284f5ec77a6548dbd76c2503b418b9067577d88 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:36:18 +0200 Subject: [PATCH 1/9] Task/harden validation of countries in asset profile endpoints (#7316) * Harden validation of countries * Update changelog --- CHANGELOG.md | 1 + libs/common/src/lib/dtos/country.dto.ts | 11 +++++++++++ libs/common/src/lib/dtos/create-asset-profile.dto.ts | 8 +++++++- libs/common/src/lib/dtos/index.ts | 2 ++ libs/common/src/lib/dtos/update-asset-profile.dto.ts | 8 +++++++- 5 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 libs/common/src/lib/dtos/country.dto.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 83146cce0..270ea6f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Hardened the validation of the countries in the asset profile endpoints - Set the change detection strategy to `OnPush` in the _X-ray_ page ## 3.25.0 - 2026-07-12 diff --git a/libs/common/src/lib/dtos/country.dto.ts b/libs/common/src/lib/dtos/country.dto.ts new file mode 100644 index 000000000..663aa1ac6 --- /dev/null +++ b/libs/common/src/lib/dtos/country.dto.ts @@ -0,0 +1,11 @@ +import { IsISO31661Alpha2, IsNumber, Max, Min } from 'class-validator'; + +export class CountryDto { + @IsISO31661Alpha2() + code: string; + + @IsNumber() + @Min(0) + @Max(1) + weight: number; +} diff --git a/libs/common/src/lib/dtos/create-asset-profile.dto.ts b/libs/common/src/lib/dtos/create-asset-profile.dto.ts index 73f0d3f5a..3e7e2224e 100644 --- a/libs/common/src/lib/dtos/create-asset-profile.dto.ts +++ b/libs/common/src/lib/dtos/create-asset-profile.dto.ts @@ -1,15 +1,19 @@ import { IsCurrencyCode } from '@ghostfolio/common/validators/is-currency-code'; import { AssetClass, AssetSubClass, DataSource, Prisma } from '@prisma/client'; +import { Type } from 'class-transformer'; import { IsArray, IsBoolean, IsEnum, IsOptional, IsString, - IsUrl + IsUrl, + ValidateNested } from 'class-validator'; +import { CountryDto } from './country.dto'; + export class CreateAssetProfileDto { @IsEnum(AssetClass) @IsOptional() @@ -25,6 +29,8 @@ export class CreateAssetProfileDto { @IsArray() @IsOptional() + @Type(() => CountryDto) + @ValidateNested({ each: true }) countries?: Prisma.InputJsonArray; @IsCurrencyCode() diff --git a/libs/common/src/lib/dtos/index.ts b/libs/common/src/lib/dtos/index.ts index cf0ce6f57..dea092398 100644 --- a/libs/common/src/lib/dtos/index.ts +++ b/libs/common/src/lib/dtos/index.ts @@ -1,4 +1,5 @@ import { AuthDeviceDto } from './auth-device.dto'; +import { CountryDto } from './country.dto'; import { CreateAccessDto } from './create-access.dto'; import { CreateAccountBalanceDto } from './create-account-balance.dto'; import { CreateAccountWithBalancesDto } from './create-account-with-balances.dto'; @@ -26,6 +27,7 @@ import { UpdateUserSettingDto } from './update-user-setting.dto'; export { AuthDeviceDto, + CountryDto, CreateAccessDto, CreateAccountBalanceDto, CreateAccountDto, diff --git a/libs/common/src/lib/dtos/update-asset-profile.dto.ts b/libs/common/src/lib/dtos/update-asset-profile.dto.ts index 7096023b3..dc660a481 100644 --- a/libs/common/src/lib/dtos/update-asset-profile.dto.ts +++ b/libs/common/src/lib/dtos/update-asset-profile.dto.ts @@ -7,6 +7,7 @@ import { DataSource, Prisma } from '@prisma/client'; +import { Type } from 'class-transformer'; import { IsArray, IsBoolean, @@ -14,9 +15,12 @@ import { IsObject, IsOptional, IsString, - IsUrl + IsUrl, + ValidateNested } from 'class-validator'; +import { CountryDto } from './country.dto'; + export class UpdateAssetProfileDto { @IsEnum(AssetClass) @IsOptional() @@ -32,6 +36,8 @@ export class UpdateAssetProfileDto { @IsArray() @IsOptional() + @Type(() => CountryDto) + @ValidateNested({ each: true }) countries?: Prisma.InputJsonArray; @IsCurrencyCode() From 6c948d39eb62ba06439d585aeec7bfa1974f435b Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:50:46 +0200 Subject: [PATCH 2/9] Task/round Fear & Greed index in Twitter bot service (#7318) * Round value * Update changelog --- CHANGELOG.md | 1 + apps/api/src/services/twitter-bot/twitter-bot.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 270ea6f09..c2b310916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Hardened the validation of the countries in the asset profile endpoints +- Rounded the value of the _Fear & Greed Index_ (market mood) in the twitter bot service - Set the change detection strategy to `OnPush` in the _X-ray_ page ## 3.25.0 - 2026-07-12 diff --git a/apps/api/src/services/twitter-bot/twitter-bot.service.ts b/apps/api/src/services/twitter-bot/twitter-bot.service.ts index 8dffddf7b..2dbed82d2 100644 --- a/apps/api/src/services/twitter-bot/twitter-bot.service.ts +++ b/apps/api/src/services/twitter-bot/twitter-bot.service.ts @@ -59,9 +59,9 @@ export class TwitterBotService implements OnModuleInit { symbolItem.marketPrice ); - let status = `Current market mood is ${emoji} ${text.toLowerCase()} (${ + let status = `Current market mood is ${emoji} ${text.toLowerCase()} (${round( symbolItem.marketPrice - }/100)`; + )}/100)`; const benchmarkListing = await this.getBenchmarkListing(); From b7fc13e20a93c07020bbaadfd2e0efad65370e58 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:01:56 +0200 Subject: [PATCH 3/9] Task/harden validation of sectors in asset profile endpoints (#7319) * Harden validation of sectors * Update changelog --- CHANGELOG.md | 1 + libs/common/src/lib/dtos/country.dto.ts | 2 +- libs/common/src/lib/dtos/create-asset-profile.dto.ts | 3 +++ libs/common/src/lib/dtos/index.ts | 2 ++ libs/common/src/lib/dtos/sector.dto.ts | 11 +++++++++++ libs/common/src/lib/dtos/update-asset-profile.dto.ts | 3 +++ 6 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 libs/common/src/lib/dtos/sector.dto.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c2b310916..cdf27bf9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Hardened the validation of the countries in the asset profile endpoints +- Hardened the validation of the sectors in the asset profile endpoints - Rounded the value of the _Fear & Greed Index_ (market mood) in the twitter bot service - Set the change detection strategy to `OnPush` in the _X-ray_ page diff --git a/libs/common/src/lib/dtos/country.dto.ts b/libs/common/src/lib/dtos/country.dto.ts index 663aa1ac6..7fc8c38ab 100644 --- a/libs/common/src/lib/dtos/country.dto.ts +++ b/libs/common/src/lib/dtos/country.dto.ts @@ -5,7 +5,7 @@ export class CountryDto { code: string; @IsNumber() - @Min(0) @Max(1) + @Min(0) weight: number; } diff --git a/libs/common/src/lib/dtos/create-asset-profile.dto.ts b/libs/common/src/lib/dtos/create-asset-profile.dto.ts index 3e7e2224e..0b0ccb9d3 100644 --- a/libs/common/src/lib/dtos/create-asset-profile.dto.ts +++ b/libs/common/src/lib/dtos/create-asset-profile.dto.ts @@ -13,6 +13,7 @@ import { } from 'class-validator'; import { CountryDto } from './country.dto'; +import { SectorDto } from './sector.dto'; export class CreateAssetProfileDto { @IsEnum(AssetClass) @@ -73,6 +74,8 @@ export class CreateAssetProfileDto { @IsArray() @IsOptional() + @Type(() => SectorDto) + @ValidateNested({ each: true }) sectors?: Prisma.InputJsonArray; @IsString() diff --git a/libs/common/src/lib/dtos/index.ts b/libs/common/src/lib/dtos/index.ts index dea092398..c74c68dfd 100644 --- a/libs/common/src/lib/dtos/index.ts +++ b/libs/common/src/lib/dtos/index.ts @@ -11,6 +11,7 @@ import { CreatePlatformDto } from './create-platform.dto'; import { CreateTagDto } from './create-tag.dto'; import { CreateWatchlistItemDto } from './create-watchlist-item.dto'; import { DeleteOwnUserDto } from './delete-own-user.dto'; +import { SectorDto } from './sector.dto'; import { TransferBalanceDto } from './transfer-balance.dto'; import { UpdateAccessDto } from './update-access.dto'; import { UpdateAccountDto } from './update-account.dto'; @@ -39,6 +40,7 @@ export { CreateTagDto, CreateWatchlistItemDto, DeleteOwnUserDto, + SectorDto, TransferBalanceDto, UpdateAccessDto, UpdateAccountDto, diff --git a/libs/common/src/lib/dtos/sector.dto.ts b/libs/common/src/lib/dtos/sector.dto.ts new file mode 100644 index 000000000..e4655cb73 --- /dev/null +++ b/libs/common/src/lib/dtos/sector.dto.ts @@ -0,0 +1,11 @@ +import { IsNumber, IsString, Max, Min } from 'class-validator'; + +export class SectorDto { + @IsString() + name: string; + + @IsNumber() + @Max(1) + @Min(0) + weight: number; +} diff --git a/libs/common/src/lib/dtos/update-asset-profile.dto.ts b/libs/common/src/lib/dtos/update-asset-profile.dto.ts index dc660a481..5b193ff7e 100644 --- a/libs/common/src/lib/dtos/update-asset-profile.dto.ts +++ b/libs/common/src/lib/dtos/update-asset-profile.dto.ts @@ -20,6 +20,7 @@ import { } from 'class-validator'; import { CountryDto } from './country.dto'; +import { SectorDto } from './sector.dto'; export class UpdateAssetProfileDto { @IsEnum(AssetClass) @@ -70,6 +71,8 @@ export class UpdateAssetProfileDto { @IsArray() @IsOptional() + @Type(() => SectorDto) + @ValidateNested({ each: true }) sectors?: Prisma.InputJsonArray; @IsOptional() From d5fb6311c14cb6e18001fbcf62eef79a587c8e84 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:05:16 +0200 Subject: [PATCH 4/9] Task/deprecate SymbolProfile in favor of assetProfile in activity interface (#7315) * Deprecate SymbolProfile in favor of assetProfile * Update changelog --- CHANGELOG.md | 1 + .../src/app/activities/activities.service.ts | 18 + .../app/endpoints/public/public.controller.ts | 7 +- apps/api/src/app/export/export.service.ts | 12 +- apps/api/src/app/import/import.service.ts | 38 +- .../portfolio-calculator-test-utils.ts | 2 +- .../calculator/portfolio-calculator.ts | 4 +- ...tfolio-calculator-baln-buy-and-buy.spec.ts | 26 +- ...aln-buy-and-sell-in-two-activities.spec.ts | 462 +++++++-------- ...folio-calculator-baln-buy-and-sell.spec.ts | 26 +- .../portfolio-calculator-baln-buy.spec.ts | 38 +- ...ulator-btceur-in-base-currency-eur.spec.ts | 14 +- .../roai/portfolio-calculator-btceur.spec.ts | 14 +- ...ator-btcusd-buy-and-sell-partially.spec.ts | 26 +- .../portfolio-calculator-btcusd-short.spec.ts | 12 +- .../roai/portfolio-calculator-btcusd.spec.ts | 12 +- .../roai/portfolio-calculator-fee.spec.ts | 14 +- .../portfolio-calculator-googl-buy.spec.ts | 14 +- ...jnug-buy-and-sell-and-buy-and-sell.spec.ts | 380 ++++++------- .../portfolio-calculator-liability.spec.ts | 14 +- ...folio-calculator-msft-buy-and-sell.spec.ts | 38 +- ...-calculator-msft-buy-with-dividend.spec.ts | 26 +- ...ulator-novn-buy-and-sell-partially.spec.ts | 12 +- ...folio-calculator-novn-buy-and-sell.spec.ts | 528 +++++++++--------- .../portfolio-calculator-valuable.spec.ts | 14 +- .../interfaces/portfolio-order.interface.ts | 2 +- .../app/portfolio/portfolio.service.spec.ts | 6 +- .../src/app/portfolio/portfolio.service.ts | 18 +- ...orm-data-source-in-response.interceptor.ts | 7 + .../activities/activities-page.component.ts | 2 +- ...ate-or-update-activity-dialog.component.ts | 30 +- .../interfaces/interfaces.ts | 4 +- .../app/services/import-activities.service.ts | 8 +- libs/common/src/lib/config.ts | 12 + .../lib/interfaces/activities.interface.ts | 8 +- .../public-portfolio-response.interface.ts | 6 + .../activities-table.component.html | 16 +- .../activities-table.component.stories.ts | 10 +- .../activities-table.component.ts | 4 +- 39 files changed, 966 insertions(+), 919 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdf27bf9b..9011a3350 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Hardened the validation of the sectors in the asset profile endpoints - Rounded the value of the _Fear & Greed Index_ (market mood) in the twitter bot service - Set the change detection strategy to `OnPush` in the _X-ray_ page +- Deprecated `SymbolProfile` in favor of `assetProfile` in the activity interface ## 3.25.0 - 2026-07-12 diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index bd08d05f9..427cddf4c 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -455,6 +455,23 @@ export class ActivitiesService { userId, accountId: account.id, accountUserId: account.userId, + assetProfile: { + activitiesCount: 0, + assetClass: AssetClass.LIQUIDITY, + assetSubClass: AssetSubClass.CASH, + countries: [], + createdAt: new Date(balanceItem.date), + currency: account.currency, + dataSource: + this.dataProviderService.getDataSourceForExchangeRates(), + holdings: [], + id: account.currency, + isActive: true, + name: account.currency, + sectors: [], + symbol: account.currency, + updatedAt: new Date(balanceItem.date) + }, comment: account.name, createdAt: new Date(balanceItem.date), currency: account.currency, @@ -823,6 +840,7 @@ export class ActivitiesService { return { ...order, + assetProfile, feeInAssetProfileCurrency, feeInBaseCurrency, unitPriceInAssetProfileCurrency, diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index f43e51f7b..a87d36218 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -110,26 +110,27 @@ export class PublicController { ? [] : activities.map( ({ + assetProfile, currency, date, fee, quantity, - SymbolProfile, type, unitPrice, value, valueInBaseCurrency }) => { return { + assetProfile, currency, date, fee, quantity, - SymbolProfile, type, unitPrice, value, - valueInBaseCurrency + valueInBaseCurrency, + SymbolProfile: assetProfile }; } ); diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 8ebfde13d..a94479562 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -124,8 +124,8 @@ export class ExportService { const customAssetProfiles = uniqBy( activities - .map(({ SymbolProfile }) => { - return SymbolProfile; + .map(({ assetProfile }) => { + return assetProfile; }) .filter(({ userId: assetProfileUserId }) => { return assetProfileUserId === userId; @@ -224,13 +224,13 @@ export class ExportService { activities: activities.map( ({ accountId, + assetProfile, comment, currency, date, fee, id, quantity, - SymbolProfile, tags: currentTags, type, unitPrice @@ -243,10 +243,10 @@ export class ExportService { quantity, type, unitPrice, - currency: currency ?? SymbolProfile.currency, - dataSource: SymbolProfile.dataSource, + currency: currency ?? assetProfile.currency, + dataSource: assetProfile.dataSource, date: date.toISOString(), - symbol: SymbolProfile.symbol, + symbol: assetProfile.symbol, tags: currentTags.map(({ id: tagId }) => { return tagId; }) diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 9b1aa417c..07019e694 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -122,11 +122,11 @@ export class ImportService { const isDuplicate = activities.some((activity) => { return ( activity.accountId === account?.id && - activity.SymbolProfile.currency === assetProfile.currency && - activity.SymbolProfile.dataSource === assetProfile.dataSource && + activity.assetProfile.currency === assetProfile.currency && + activity.assetProfile.dataSource === assetProfile.dataSource && isSameSecond(activity.date, date) && activity.quantity === quantity && - activity.SymbolProfile.symbol === assetProfile.symbol && + activity.assetProfile.symbol === assetProfile.symbol && activity.type === 'DIVIDEND' && activity.unitPrice === marketPrice ); @@ -138,6 +138,7 @@ export class ImportService { return { account, + assetProfile, date, error, quantity, @@ -463,19 +464,18 @@ export class ImportService { const error = activity.error; const fee = activity.fee; const quantity = activity.quantity; - const SymbolProfile = activity.SymbolProfile; const tagIds = activity.tagIds ?? []; const type = activity.type; const unitPrice = activity.unitPrice; const assetProfile = assetProfiles[ getAssetProfileIdentifier({ - dataSource: SymbolProfile.dataSource, - symbol: SymbolProfile.symbol + dataSource: activity.assetProfile.dataSource, + symbol: activity.assetProfile.symbol }) ] ?? { - dataSource: SymbolProfile.dataSource, - symbol: SymbolProfile.symbol + dataSource: activity.assetProfile.dataSource, + symbol: activity.assetProfile.symbol }; const { assetClass, @@ -619,6 +619,8 @@ export class ImportService { activities.push({ ...order, + // @ts-ignore + assetProfile, error, value, valueInBaseCurrency, @@ -633,19 +635,19 @@ export class ImportService { if (!isDryRun) { // Gather symbol data in the background, if not dry run - const uniqueActivities = uniqBy(activities, ({ SymbolProfile }) => { + const uniqueActivities = uniqBy(activities, ({ assetProfile }) => { return getAssetProfileIdentifier({ - dataSource: SymbolProfile.dataSource, - symbol: SymbolProfile.symbol + dataSource: assetProfile.dataSource, + symbol: assetProfile.symbol }); }); this.dataGatheringService.gatherSymbols({ - dataGatheringItems: uniqueActivities.map(({ date, SymbolProfile }) => { + dataGatheringItems: uniqueActivities.map(({ assetProfile, date }) => { return { date, - dataSource: SymbolProfile.dataSource, - symbol: SymbolProfile.symbol + dataSource: assetProfile.dataSource, + symbol: assetProfile.symbol }; }), priority: DATA_GATHERING_QUEUE_PRIORITY_HIGH @@ -692,12 +694,12 @@ export class ImportService { activity.accountId === accountId && activity.comment === comment && (activity.currency === currency || - activity.SymbolProfile.currency === currency) && - activity.SymbolProfile.dataSource === dataSource && + activity.assetProfile.currency === currency) && + activity.assetProfile.dataSource === dataSource && isSameSecond(activity.date, date) && activity.fee === fee && activity.quantity === quantity && - activity.SymbolProfile.symbol === symbol && + activity.assetProfile.symbol === symbol && activity.type === type && activity.unitPrice === unitPrice ); @@ -717,7 +719,7 @@ export class ImportService { quantity, type, unitPrice, - SymbolProfile: { + assetProfile: { dataSource, symbol, activitiesCount: undefined, 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 f4c99916f..5e6bfba99 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 @@ -22,7 +22,7 @@ export const activityDummyData = { valueInBaseCurrency: undefined }; -export const symbolProfileDummyData = { +export const assetProfileDummyData = { activitiesCount: undefined, assetClass: undefined, assetSubClass: undefined, diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index e73e222ac..cee94f020 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -121,11 +121,11 @@ export abstract class PortfolioCalculator { this.activities = activities .map( ({ + assetProfile, date, feeInAssetProfileCurrency, feeInBaseCurrency, quantity, - SymbolProfile, tags = [], type, unitPriceInAssetProfileCurrency @@ -141,9 +141,9 @@ export abstract class PortfolioCalculator { } return { + assetProfile, tags, type, - assetProfile: SymbolProfile, date: format(date, DATE_FORMAT), fee: new Big(feeInAssetProfileCurrency), feeInBaseCurrency: new Big(feeInBaseCurrency), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts index f21418cb4..fe23af04b 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,33 +85,33 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-11-22'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-22'), + feeInAssetProfileCurrency: 1.55, + feeInBaseCurrency: 1.55, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 142.9 }, { ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.65, - feeInBaseCurrency: 1.65, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.65, + feeInBaseCurrency: 1.65, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 136.6 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index e2cba2e61..061aaf817 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -1,231 +1,231 @@ -import { - activityDummyData, - 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 { Activity } from '@ghostfolio/common/interfaces'; -import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; - -import { Big } from 'big.js'; - -jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { - return { - CurrentRateService: jest.fn().mockImplementation(() => { - return CurrentRateServiceMock; - }) - }; -}); - -jest.mock( - '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', - () => { - return { - PortfolioSnapshotService: jest.fn().mockImplementation(() => { - return PortfolioSnapshotServiceMock; - }) - }; - } -); - -jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { - return { - RedisCacheService: jest.fn().mockImplementation(() => { - return RedisCacheServiceMock; - }) - }; -}); - -describe('PortfolioCalculator', () => { - let configurationService: ConfigurationService; - let currentRateService: CurrentRateService; - let exchangeRateDataService: ExchangeRateDataService; - let portfolioCalculatorFactory: PortfolioCalculatorFactory; - let portfolioSnapshotService: PortfolioSnapshotService; - let redisCacheService: RedisCacheService; - - beforeEach(() => { - configurationService = new ConfigurationService(); - - currentRateService = new CurrentRateService(null, null, null, null); - - exchangeRateDataService = new ExchangeRateDataService( - null, - null, - null, - null - ); - - portfolioSnapshotService = new PortfolioSnapshotService(null, null); - - redisCacheService = new RedisCacheService(null, null); - - portfolioCalculatorFactory = new PortfolioCalculatorFactory( - configurationService, - currentRateService, - exchangeRateDataService, - portfolioSnapshotService, - redisCacheService - ); - }); - - describe('get current positions', () => { - it.only('with BALN.SW buy and sell in two activities', async () => { - jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); - - const activities: Activity[] = [ - { - ...activityDummyData, - date: new Date('2021-11-22'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - type: 'BUY', - unitPriceInAssetProfileCurrency: 142.9 - }, - { - ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.65, - feeInBaseCurrency: 1.65, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - type: 'SELL', - unitPriceInAssetProfileCurrency: 136.6 - }, - { - ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, - currency: 'CHF', - dataSource: 'YAHOO', - name: 'Bâloise Holding AG', - symbol: 'BALN.SW' - }, - type: 'SELL', - unitPriceInAssetProfileCurrency: 136.6 - } - ]; - - const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ - activities, - calculationType: PerformanceCalculationType.ROAI, - currency: 'CHF', - userId: userDummyData.id - }); - - const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); - - const investments = portfolioCalculator.getInvestments(); - - const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'month' - }); - - const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'year' - }); - - expect(portfolioSnapshot).toMatchObject({ - currentValueInBaseCurrency: new Big('0'), - errors: [], - hasErrors: false, - positions: [ - { - activitiesCount: 3, - averagePrice: new Big('0'), - currency: 'CHF', - dataSource: 'YAHOO', - dateOfFirstActivity: '2021-11-22', - dividend: new Big('0'), - dividendInBaseCurrency: new Big('0'), - fee: new Big('3.2'), - feeInBaseCurrency: new Big('3.2'), - grossPerformance: new Big('-12.6'), - grossPerformancePercentage: new Big('-0.04408677396780965649'), - grossPerformancePercentageWithCurrencyEffect: new Big( - '-0.04408677396780965649' - ), - grossPerformanceWithCurrencyEffect: new Big('-12.6'), - investment: new Big('0'), - investmentWithCurrencyEffect: new Big('0'), - netPerformancePercentageWithCurrencyEffectMap: { - max: new Big('-0.0552834149755073478') - }, - netPerformanceWithCurrencyEffectMap: { - max: new Big('-15.8') - }, - marketPrice: 148.9, - marketPriceInBaseCurrency: 148.9, - quantity: new Big('0'), - symbol: 'BALN.SW', - tags: [], - timeWeightedInvestment: new Big('285.80000000000000396627'), - timeWeightedInvestmentWithCurrencyEffect: new Big( - '285.80000000000000396627' - ), - valueInBaseCurrency: new Big('0') - } - ], - totalFeesWithCurrencyEffect: new Big('3.2'), - totalInterestWithCurrencyEffect: new Big('0'), - totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0'), - totalLiabilitiesWithCurrencyEffect: new Big('0') - }); - - expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( - expect.objectContaining({ - netPerformance: -15.8, - netPerformanceInPercentage: -0.05528341497550734703, - netPerformanceInPercentageWithCurrencyEffect: -0.05528341497550734703, - netPerformanceWithCurrencyEffect: -15.8, - totalInvestment: 0, - totalInvestmentValueWithCurrencyEffect: 0 - }) - ); - - expect(investments).toEqual([ - { date: '2021-11-22', investment: new Big('285.8') }, - { date: '2021-11-30', investment: new Big('0') } - ]); - - expect(investmentsByMonth).toEqual([ - { date: '2021-11-01', investment: 0 }, - { date: '2021-12-01', investment: 0 } - ]); - - expect(investmentsByYear).toEqual([ - { date: '2021-01-01', investment: 0 } - ]); - }); - }); -}); +import { + activityDummyData, + assetProfileDummyData, + 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 { Activity } from '@ghostfolio/common/interfaces'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; + +import { Big } from 'big.js'; + +jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => { + return { + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', + () => { + return { + PortfolioSnapshotService: jest.fn().mockImplementation(() => { + return PortfolioSnapshotServiceMock; + }) + }; + } +); + +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let configurationService: ConfigurationService; + let currentRateService: CurrentRateService; + let exchangeRateDataService: ExchangeRateDataService; + let portfolioCalculatorFactory: PortfolioCalculatorFactory; + let portfolioSnapshotService: PortfolioSnapshotService; + let redisCacheService: RedisCacheService; + + beforeEach(() => { + configurationService = new ConfigurationService(); + + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + portfolioSnapshotService = new PortfolioSnapshotService(null, null); + + redisCacheService = new RedisCacheService(null, null); + + portfolioCalculatorFactory = new PortfolioCalculatorFactory( + configurationService, + currentRateService, + exchangeRateDataService, + portfolioSnapshotService, + redisCacheService + ); + }); + + describe('get current positions', () => { + it.only('with BALN.SW buy and sell in two activities', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2021-12-18').getTime()); + + const activities: Activity[] = [ + { + ...activityDummyData, + assetProfile: { + ...assetProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' + }, + date: new Date('2021-11-22'), + feeInAssetProfileCurrency: 1.55, + feeInBaseCurrency: 1.55, + quantity: 2, + type: 'BUY', + unitPriceInAssetProfileCurrency: 142.9 + }, + { + ...activityDummyData, + assetProfile: { + ...assetProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' + }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.65, + feeInBaseCurrency: 1.65, + quantity: 1, + type: 'SELL', + unitPriceInAssetProfileCurrency: 136.6 + }, + { + ...activityDummyData, + assetProfile: { + ...assetProfileDummyData, + currency: 'CHF', + dataSource: 'YAHOO', + name: 'Bâloise Holding AG', + symbol: 'BALN.SW' + }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, + type: 'SELL', + unitPriceInAssetProfileCurrency: 136.6 + } + ]; + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: 'CHF', + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + const investments = portfolioCalculator.getInvestments(); + + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'month' + }); + + const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); + + expect(portfolioSnapshot).toMatchObject({ + currentValueInBaseCurrency: new Big('0'), + errors: [], + hasErrors: false, + positions: [ + { + activitiesCount: 3, + averagePrice: new Big('0'), + currency: 'CHF', + dataSource: 'YAHOO', + dateOfFirstActivity: '2021-11-22', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('3.2'), + feeInBaseCurrency: new Big('3.2'), + grossPerformance: new Big('-12.6'), + grossPerformancePercentage: new Big('-0.04408677396780965649'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '-0.04408677396780965649' + ), + grossPerformanceWithCurrencyEffect: new Big('-12.6'), + investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), + netPerformancePercentageWithCurrencyEffectMap: { + max: new Big('-0.0552834149755073478') + }, + netPerformanceWithCurrencyEffectMap: { + max: new Big('-15.8') + }, + marketPrice: 148.9, + marketPriceInBaseCurrency: 148.9, + quantity: new Big('0'), + symbol: 'BALN.SW', + tags: [], + timeWeightedInvestment: new Big('285.80000000000000396627'), + timeWeightedInvestmentWithCurrencyEffect: new Big( + '285.80000000000000396627' + ), + valueInBaseCurrency: new Big('0') + } + ], + totalFeesWithCurrencyEffect: new Big('3.2'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0') + }); + + expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( + expect.objectContaining({ + netPerformance: -15.8, + netPerformanceInPercentage: -0.05528341497550734703, + netPerformanceInPercentageWithCurrencyEffect: -0.05528341497550734703, + netPerformanceWithCurrencyEffect: -15.8, + totalInvestment: 0, + totalInvestmentValueWithCurrencyEffect: 0 + }) + ); + + expect(investments).toEqual([ + { date: '2021-11-22', investment: new Big('285.8') }, + { date: '2021-11-30', investment: new Big('0') } + ]); + + expect(investmentsByMonth).toEqual([ + { date: '2021-11-01', investment: 0 }, + { date: '2021-12-01', investment: 0 } + ]); + + expect(investmentsByYear).toEqual([ + { date: '2021-01-01', investment: 0 } + ]); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts index fa397de46..c8e0af46d 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,33 +85,33 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-11-22'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-22'), + feeInAssetProfileCurrency: 1.55, + feeInBaseCurrency: 1.55, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 142.9 }, { ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.65, - feeInBaseCurrency: 1.65, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.65, + feeInBaseCurrency: 1.65, + quantity: 2, type: 'SELL', unitPriceInAssetProfileCurrency: 136.6 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts index db44fe0ae..4922382a9 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.55, + feeInBaseCurrency: 1.55, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 136.6 } @@ -217,17 +217,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.55, + feeInBaseCurrency: 1.55, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 135.0 } @@ -257,17 +257,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-11-30'), - feeInAssetProfileCurrency: 1.55, - feeInBaseCurrency: 1.55, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'CHF', dataSource: 'YAHOO', name: 'Bâloise Holding AG', symbol: 'BALN.SW' }, + date: new Date('2021-11-30'), + feeInAssetProfileCurrency: 1.55, + feeInBaseCurrency: 1.55, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 135.0 } 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 1afd9225d..21a8d2056 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 @@ -1,7 +1,7 @@ import { activityDummyData, + assetProfileDummyData, loadExportFile, - symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -107,17 +107,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = exportResponse.activities.map( (activity) => ({ ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: 4.46, - feeInBaseCurrency: 3.94, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: activity.dataSource, name: 'Bitcoin', symbol: activity.symbol }, + ...activity, + date: parseDate(activity.date), + feeInAssetProfileCurrency: 4.46, + feeInBaseCurrency: 3.94, unitPriceInAssetProfileCurrency: 44558.42 }) ); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts index 0e5750166..bc80e8996 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts @@ -1,7 +1,7 @@ import { activityDummyData, + assetProfileDummyData, loadExportFile, - symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -95,17 +95,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = exportResponse.activities.map( (activity) => ({ ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: 4.46, - feeInBaseCurrency: 4.46, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: activity.dataSource, name: 'Bitcoin', symbol: activity.symbol }, + ...activity, + date: parseDate(activity.date), + feeInAssetProfileCurrency: 4.46, + feeInBaseCurrency: 4.46, unitPriceInAssetProfileCurrency: 44558.42 }) ); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 1e556735d..f20506b06 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -98,33 +98,33 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2015-01-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 2, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Bitcoin USD', symbol: 'BTCUSD' }, + date: new Date('2015-01-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 2, type: 'BUY', unitPriceInAssetProfileCurrency: 320.43 }, { ...activityDummyData, - date: new Date('2017-12-31'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Bitcoin USD', symbol: 'BTCUSD' }, + date: new Date('2017-12-31'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, type: 'SELL', unitPriceInAssetProfileCurrency: 14156.4 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts index 9d84540e3..79c6979ef 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-short.spec.ts @@ -1,7 +1,7 @@ import { activityDummyData, + assetProfileDummyData, loadExportFile, - symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -96,16 +96,16 @@ describe('PortfolioCalculator', () => { (activity) => ({ ...activityDummyData, ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - feeInBaseCurrency: activity.fee, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: activity.dataSource, name: 'Bitcoin', symbol: activity.symbol }, + date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, + feeInBaseCurrency: activity.fee, unitPriceInAssetProfileCurrency: activity.unitPrice }) ); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts index 0916e18a4..18b0e6d64 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts @@ -1,7 +1,7 @@ import { activityDummyData, + assetProfileDummyData, loadExportFile, - symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -96,16 +96,16 @@ describe('PortfolioCalculator', () => { (activity) => ({ ...activityDummyData, ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: 4.46, - feeInBaseCurrency: 4.46, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: activity.dataSource, name: 'Bitcoin', symbol: activity.symbol }, + date: parseDate(activity.date), + feeInAssetProfileCurrency: 4.46, + feeInBaseCurrency: 4.46, unitPriceInAssetProfileCurrency: 44558.42 }) ); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts index b55c94b2b..e5798818f 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-09-01'), - feeInAssetProfileCurrency: 49, - feeInBaseCurrency: 49, - quantity: 0, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'MANUAL', name: 'Account Opening Fee', symbol: '2c463fb3-af07-486e-adb0-8301b3d72141' }, + date: new Date('2021-09-01'), + feeInAssetProfileCurrency: 49, + feeInBaseCurrency: 49, + quantity: 0, type: 'FEE', unitPriceInAssetProfileCurrency: 0 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts index bb61d637e..984dc1154 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -97,17 +97,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2023-01-03'), - feeInAssetProfileCurrency: 1, - feeInBaseCurrency: 0.9238, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Alphabet Inc.', symbol: 'GOOGL' }, + date: new Date('2023-01-03'), + feeInAssetProfileCurrency: 1, + feeInBaseCurrency: 0.9238, + quantity: 1, type: 'BUY', unitPriceInAssetProfileCurrency: 89.12 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts index 88afc971d..962cfe2d8 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts @@ -1,190 +1,190 @@ -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 { Activity, ExportResponse } 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 { - CurrentRateService: jest.fn().mockImplementation(() => { - return CurrentRateServiceMock; - }) - }; -}); - -jest.mock( - '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', - () => { - return { - PortfolioSnapshotService: jest.fn().mockImplementation(() => { - return PortfolioSnapshotServiceMock; - }) - }; - } -); - -jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { - return { - RedisCacheService: jest.fn().mockImplementation(() => { - return RedisCacheServiceMock; - }) - }; -}); - -describe('PortfolioCalculator', () => { - let exportResponse: ExportResponse; - - 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/jnug-buy-and-sell-and-buy-and-sell.json' - ) - ); - }); - - beforeEach(() => { - configurationService = new ConfigurationService(); - - currentRateService = new CurrentRateService(null, null, null, null); - - exchangeRateDataService = new ExchangeRateDataService( - null, - null, - null, - null - ); - - portfolioSnapshotService = new PortfolioSnapshotService(null, null); - - redisCacheService = new RedisCacheService(null, null); - - portfolioCalculatorFactory = new PortfolioCalculatorFactory( - configurationService, - currentRateService, - exchangeRateDataService, - portfolioSnapshotService, - redisCacheService - ); - }); - - describe('get current positions', () => { - it.only('with JNUG buy and sell', async () => { - jest.useFakeTimers().setSystemTime(parseDate('2025-12-28').getTime()); - - const activities: Activity[] = exportResponse.activities.map( - (activity) => ({ - ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - feeInBaseCurrency: activity.fee, - SymbolProfile: { - ...symbolProfileDummyData, - currency: activity.currency, - dataSource: activity.dataSource, - name: 'Direxion Daily Junior Gold Miners Index Bull 2X Shares', - symbol: activity.symbol - }, - unitPriceInAssetProfileCurrency: activity.unitPrice - }) - ); - - const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ - activities, - calculationType: PerformanceCalculationType.ROAI, - currency: exportResponse.user.settings.currency, - userId: userDummyData.id - }); - - const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); - - const investments = portfolioCalculator.getInvestments(); - - const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'month' - }); - - const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'year' - }); - - expect(portfolioSnapshot).toMatchObject({ - currentValueInBaseCurrency: new Big('0'), - errors: [], - hasErrors: false, - positions: [ - { - activitiesCount: 4, - averagePrice: new Big('0'), - currency: 'USD', - dataSource: 'YAHOO', - dateOfFirstActivity: '2025-12-11', - dividend: new Big('0'), - dividendInBaseCurrency: new Big('0'), - fee: new Big('4'), - feeInBaseCurrency: new Big('4'), - grossPerformance: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) - grossPerformanceWithCurrencyEffect: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) - investment: new Big('0'), - investmentWithCurrencyEffect: new Big('0'), - netPerformance: new Big('39.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4 - netPerformanceWithCurrencyEffectMap: { - max: new Big('39.95') // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4 - }, - marketPrice: 237.8000030517578, - marketPriceInBaseCurrency: 237.8000030517578, - quantity: new Big('0'), - symbol: 'JNUG', - tags: [], - valueInBaseCurrency: new Big('0') - } - ], - totalFeesWithCurrencyEffect: new Big('4'), - totalInterestWithCurrencyEffect: new Big('0'), - totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0'), - totalLiabilitiesWithCurrencyEffect: new Big('0') - }); - - expect(investments).toEqual([ - { date: '2025-12-11', investment: new Big('1885.05') }, - { date: '2025-12-18', investment: new Big('2041.1') }, - { date: '2025-12-28', investment: new Big('0') } - ]); - - expect(investmentsByMonth).toEqual([ - { date: '2025-12-01', investment: 0 } - ]); - - expect(investmentsByYear).toEqual([ - { date: '2025-01-01', investment: 0 } - ]); - }); - }); -}); +import { + activityDummyData, + assetProfileDummyData, + loadExportFile, + 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 { Activity, ExportResponse } 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 { + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', + () => { + return { + PortfolioSnapshotService: jest.fn().mockImplementation(() => { + return PortfolioSnapshotServiceMock; + }) + }; + } +); + +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let exportResponse: ExportResponse; + + 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/jnug-buy-and-sell-and-buy-and-sell.json' + ) + ); + }); + + beforeEach(() => { + configurationService = new ConfigurationService(); + + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + portfolioSnapshotService = new PortfolioSnapshotService(null, null); + + redisCacheService = new RedisCacheService(null, null); + + portfolioCalculatorFactory = new PortfolioCalculatorFactory( + configurationService, + currentRateService, + exchangeRateDataService, + portfolioSnapshotService, + redisCacheService + ); + }); + + describe('get current positions', () => { + it.only('with JNUG buy and sell', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2025-12-28').getTime()); + + const activities: Activity[] = exportResponse.activities.map( + (activity) => ({ + ...activityDummyData, + ...activity, + assetProfile: { + ...assetProfileDummyData, + currency: activity.currency, + dataSource: activity.dataSource, + name: 'Direxion Daily Junior Gold Miners Index Bull 2X Shares', + symbol: activity.symbol + }, + date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, + feeInBaseCurrency: activity.fee, + unitPriceInAssetProfileCurrency: activity.unitPrice + }) + ); + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: exportResponse.user.settings.currency, + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + const investments = portfolioCalculator.getInvestments(); + + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'month' + }); + + const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); + + expect(portfolioSnapshot).toMatchObject({ + currentValueInBaseCurrency: new Big('0'), + errors: [], + hasErrors: false, + positions: [ + { + activitiesCount: 4, + averagePrice: new Big('0'), + currency: 'USD', + dataSource: 'YAHOO', + dateOfFirstActivity: '2025-12-11', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('4'), + feeInBaseCurrency: new Big('4'), + grossPerformance: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) + grossPerformanceWithCurrencyEffect: new Big('43.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) + investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), + netPerformance: new Big('39.95'), // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4 + netPerformanceWithCurrencyEffectMap: { + max: new Big('39.95') // (1890.00 - 1885.05) + (2080.10 - 2041.10) - 4 + }, + marketPrice: 237.8000030517578, + marketPriceInBaseCurrency: 237.8000030517578, + quantity: new Big('0'), + symbol: 'JNUG', + tags: [], + valueInBaseCurrency: new Big('0') + } + ], + totalFeesWithCurrencyEffect: new Big('4'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0') + }); + + expect(investments).toEqual([ + { date: '2025-12-11', investment: new Big('1885.05') }, + { date: '2025-12-18', investment: new Big('2041.1') }, + { date: '2025-12-28', investment: new Big('0') } + ]); + + expect(investmentsByMonth).toEqual([ + { date: '2025-12-01', investment: 0 } + ]); + + expect(investmentsByYear).toEqual([ + { date: '2025-01-01', investment: 0 } + ]); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts index 4f2058513..94654af61 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-liability.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2023-01-01'), // Date in future - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'MANUAL', name: 'Loan', symbol: '55196015-1365-4560-aa60-8751ae6d18f8' }, + date: new Date('2023-01-01'), // Date in future + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, type: 'LIABILITY', unitPriceInAssetProfileCurrency: 3000 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts index 36d1ea1ec..b1f030aae 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-and-sell.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -78,49 +78,49 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2024-03-08'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 0.3333333333333333, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Microsoft Inc.', symbol: 'MSFT' }, + date: new Date('2024-03-08'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 0.3333333333333333, type: 'BUY', unitPriceInAssetProfileCurrency: 408 }, { ...activityDummyData, - date: new Date('2024-03-13'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 0.6666666666666666, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Microsoft Inc.', symbol: 'MSFT' }, + date: new Date('2024-03-13'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 0.6666666666666666, type: 'BUY', unitPriceInAssetProfileCurrency: 400 }, { ...activityDummyData, - date: new Date('2024-03-14'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Microsoft Inc.', symbol: 'MSFT' }, + date: new Date('2024-03-14'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, type: 'SELL', unitPriceInAssetProfileCurrency: 411 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts index c50ef2b34..91c095623 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-with-dividend.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,33 +85,33 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2021-09-16'), - feeInAssetProfileCurrency: 19, - feeInBaseCurrency: 19, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Microsoft Inc.', symbol: 'MSFT' }, + date: new Date('2021-09-16'), + feeInAssetProfileCurrency: 19, + feeInBaseCurrency: 19, + quantity: 1, type: 'BUY', unitPriceInAssetProfileCurrency: 298.58 }, { ...activityDummyData, - date: new Date('2021-11-16'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'YAHOO', name: 'Microsoft Inc.', symbol: 'MSFT' }, + date: new Date('2021-11-16'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, type: 'DIVIDEND', unitPriceInAssetProfileCurrency: 0.62 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index cdfc78906..8ce62db59 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -1,7 +1,7 @@ import { activityDummyData, + assetProfileDummyData, loadExportFile, - symbolProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -99,16 +99,16 @@ describe('PortfolioCalculator', () => { (activity) => ({ ...activityDummyData, ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - feeInBaseCurrency: activity.fee, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: activity.currency, dataSource: activity.dataSource, name: 'Novartis AG', symbol: activity.symbol }, + date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, + feeInBaseCurrency: activity.fee, unitPriceInAssetProfileCurrency: activity.unitPrice }) ); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts index 2cd754c82..f86b23d9a 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell.spec.ts @@ -1,264 +1,264 @@ -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 { Activity, ExportResponse } 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 { - CurrentRateService: jest.fn().mockImplementation(() => { - return CurrentRateServiceMock; - }) - }; -}); - -jest.mock( - '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', - () => { - return { - PortfolioSnapshotService: jest.fn().mockImplementation(() => { - return PortfolioSnapshotServiceMock; - }) - }; - } -); - -jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { - return { - RedisCacheService: jest.fn().mockImplementation(() => { - return RedisCacheServiceMock; - }) - }; -}); - -describe('PortfolioCalculator', () => { - let exportResponse: ExportResponse; - - 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/novn-buy-and-sell.json' - ) - ); - }); - - beforeEach(() => { - configurationService = new ConfigurationService(); - - currentRateService = new CurrentRateService(null, null, null, null); - - exchangeRateDataService = new ExchangeRateDataService( - null, - null, - null, - null - ); - - portfolioSnapshotService = new PortfolioSnapshotService(null, null); - - redisCacheService = new RedisCacheService(null, null); - - portfolioCalculatorFactory = new PortfolioCalculatorFactory( - configurationService, - currentRateService, - exchangeRateDataService, - portfolioSnapshotService, - redisCacheService - ); - }); - - describe('get current positions', () => { - it.only('with NOVN.SW buy and sell', async () => { - jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); - - const activities: Activity[] = exportResponse.activities.map( - (activity) => ({ - ...activityDummyData, - ...activity, - date: parseDate(activity.date), - feeInAssetProfileCurrency: activity.fee, - feeInBaseCurrency: activity.fee, - SymbolProfile: { - ...symbolProfileDummyData, - currency: activity.currency, - dataSource: activity.dataSource, - name: 'Novartis AG', - symbol: activity.symbol - }, - unitPriceInAssetProfileCurrency: activity.unitPrice - }) - ); - - const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ - activities, - calculationType: PerformanceCalculationType.ROAI, - currency: exportResponse.user.settings.currency, - userId: userDummyData.id - }); - - const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); - - const investments = portfolioCalculator.getInvestments(); - - const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'month' - }); - - const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ - data: portfolioSnapshot.historicalData, - groupBy: 'year' - }); - - expect(portfolioSnapshot.historicalData[0]).toEqual({ - date: '2022-03-06', - investmentValueWithCurrencyEffect: 0, - netPerformance: 0, - netPerformanceInPercentage: 0, - netPerformanceInPercentageWithCurrencyEffect: 0, - netPerformanceWithCurrencyEffect: 0, - netWorth: 0, - totalAccountBalance: 0, - totalInvestment: 0, - totalInvestmentValueWithCurrencyEffect: 0, - value: 0, - valueWithCurrencyEffect: 0 - }); - - /** - * Closing price on 2022-03-07 is unknown, - * hence it uses the last unit price (2022-04-11): 87.8 - */ - expect(portfolioSnapshot.historicalData[1]).toEqual({ - date: '2022-03-07', - investmentValueWithCurrencyEffect: 151.6, - netPerformance: 24, // 2 * (87.8 - 75.8) = 24 - netPerformanceInPercentage: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 - netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 - netPerformanceWithCurrencyEffect: 24, - netWorth: 175.6, // 2 * 87.8 = 175.6 - totalAccountBalance: 0, - totalInvestment: 151.6, - totalInvestmentValueWithCurrencyEffect: 151.6, - value: 175.6, // 2 * 87.8 = 175.6 - valueWithCurrencyEffect: 175.6 - }); - - expect( - portfolioSnapshot.historicalData[ - portfolioSnapshot.historicalData.length - 1 - ] - ).toEqual({ - date: '2022-04-11', - investmentValueWithCurrencyEffect: 0, - netPerformance: 19.86, - netPerformanceInPercentage: 0.13100263852242744, - netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, - netPerformanceWithCurrencyEffect: 19.86, - netWorth: 0, - totalAccountBalance: 0, - totalInvestment: 0, - totalInvestmentValueWithCurrencyEffect: 0, - value: 0, - valueWithCurrencyEffect: 0 - }); - - expect(portfolioSnapshot).toMatchObject({ - currentValueInBaseCurrency: new Big('0'), - errors: [], - hasErrors: false, - positions: [ - { - activitiesCount: 2, - averagePrice: new Big('0'), - currency: 'CHF', - dataSource: 'YAHOO', - dateOfFirstActivity: '2022-03-07', - dividend: new Big('0'), - dividendInBaseCurrency: new Big('0'), - fee: new Big('0'), - feeInBaseCurrency: new Big('0'), - grossPerformance: new Big('19.86'), - grossPerformancePercentage: new Big('0.13100263852242744063'), - grossPerformancePercentageWithCurrencyEffect: new Big( - '0.13100263852242744063' - ), - grossPerformanceWithCurrencyEffect: new Big('19.86'), - investment: new Big('0'), - investmentWithCurrencyEffect: new Big('0'), - netPerformance: new Big('19.86'), - netPerformancePercentage: new Big('0.13100263852242744063'), - netPerformancePercentageWithCurrencyEffectMap: { - max: new Big('0.13100263852242744063') - }, - netPerformanceWithCurrencyEffectMap: { - max: new Big('19.86') - }, - marketPrice: 87.8, - marketPriceInBaseCurrency: 87.8, - quantity: new Big('0'), - symbol: 'NOVN.SW', - tags: [], - timeWeightedInvestment: new Big('151.6'), - timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), - valueInBaseCurrency: new Big('0') - } - ], - totalFeesWithCurrencyEffect: new Big('0'), - totalInterestWithCurrencyEffect: new Big('0'), - totalInvestment: new Big('0'), - totalInvestmentWithCurrencyEffect: new Big('0'), - totalLiabilitiesWithCurrencyEffect: new Big('0') - }); - - expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( - expect.objectContaining({ - netPerformance: 19.86, - netPerformanceInPercentage: 0.13100263852242744063, - netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063, - netPerformanceWithCurrencyEffect: 19.86, - totalInvestment: 0, - totalInvestmentValueWithCurrencyEffect: 0 - }) - ); - - expect(investments).toEqual([ - { date: '2022-03-07', investment: new Big('151.6') }, - { date: '2022-04-08', investment: new Big('0') } - ]); - - expect(investmentsByMonth).toEqual([ - { date: '2022-03-01', investment: 151.6 }, - { date: '2022-04-01', investment: -151.6 } - ]); - - expect(investmentsByYear).toEqual([ - { date: '2022-01-01', investment: 0 } - ]); - }); - }); -}); +import { + activityDummyData, + assetProfileDummyData, + loadExportFile, + 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 { Activity, ExportResponse } 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 { + CurrentRateService: jest.fn().mockImplementation(() => { + return CurrentRateServiceMock; + }) + }; +}); + +jest.mock( + '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service', + () => { + return { + PortfolioSnapshotService: jest.fn().mockImplementation(() => { + return PortfolioSnapshotServiceMock; + }) + }; + } +); + +jest.mock('@ghostfolio/api/app/redis-cache/redis-cache.service', () => { + return { + RedisCacheService: jest.fn().mockImplementation(() => { + return RedisCacheServiceMock; + }) + }; +}); + +describe('PortfolioCalculator', () => { + let exportResponse: ExportResponse; + + 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/novn-buy-and-sell.json' + ) + ); + }); + + beforeEach(() => { + configurationService = new ConfigurationService(); + + currentRateService = new CurrentRateService(null, null, null, null); + + exchangeRateDataService = new ExchangeRateDataService( + null, + null, + null, + null + ); + + portfolioSnapshotService = new PortfolioSnapshotService(null, null); + + redisCacheService = new RedisCacheService(null, null); + + portfolioCalculatorFactory = new PortfolioCalculatorFactory( + configurationService, + currentRateService, + exchangeRateDataService, + portfolioSnapshotService, + redisCacheService + ); + }); + + describe('get current positions', () => { + it.only('with NOVN.SW buy and sell', async () => { + jest.useFakeTimers().setSystemTime(parseDate('2022-04-11').getTime()); + + const activities: Activity[] = exportResponse.activities.map( + (activity) => ({ + ...activityDummyData, + ...activity, + assetProfile: { + ...assetProfileDummyData, + currency: activity.currency, + dataSource: activity.dataSource, + name: 'Novartis AG', + symbol: activity.symbol + }, + date: parseDate(activity.date), + feeInAssetProfileCurrency: activity.fee, + feeInBaseCurrency: activity.fee, + unitPriceInAssetProfileCurrency: activity.unitPrice + }) + ); + + const portfolioCalculator = portfolioCalculatorFactory.createCalculator({ + activities, + calculationType: PerformanceCalculationType.ROAI, + currency: exportResponse.user.settings.currency, + userId: userDummyData.id + }); + + const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); + + const investments = portfolioCalculator.getInvestments(); + + const investmentsByMonth = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'month' + }); + + const investmentsByYear = portfolioCalculator.getInvestmentsByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); + + expect(portfolioSnapshot.historicalData[0]).toEqual({ + date: '2022-03-06', + investmentValueWithCurrencyEffect: 0, + netPerformance: 0, + netPerformanceInPercentage: 0, + netPerformanceInPercentageWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0, + netWorth: 0, + totalAccountBalance: 0, + totalInvestment: 0, + totalInvestmentValueWithCurrencyEffect: 0, + value: 0, + valueWithCurrencyEffect: 0 + }); + + /** + * Closing price on 2022-03-07 is unknown, + * hence it uses the last unit price (2022-04-11): 87.8 + */ + expect(portfolioSnapshot.historicalData[1]).toEqual({ + date: '2022-03-07', + investmentValueWithCurrencyEffect: 151.6, + netPerformance: 24, // 2 * (87.8 - 75.8) = 24 + netPerformanceInPercentage: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 + netPerformanceInPercentageWithCurrencyEffect: 0.158311345646438, // 24 ÷ 151.6 = 0.158311345646438 + netPerformanceWithCurrencyEffect: 24, + netWorth: 175.6, // 2 * 87.8 = 175.6 + totalAccountBalance: 0, + totalInvestment: 151.6, + totalInvestmentValueWithCurrencyEffect: 151.6, + value: 175.6, // 2 * 87.8 = 175.6 + valueWithCurrencyEffect: 175.6 + }); + + expect( + portfolioSnapshot.historicalData[ + portfolioSnapshot.historicalData.length - 1 + ] + ).toEqual({ + date: '2022-04-11', + investmentValueWithCurrencyEffect: 0, + netPerformance: 19.86, + netPerformanceInPercentage: 0.13100263852242744, + netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744, + netPerformanceWithCurrencyEffect: 19.86, + netWorth: 0, + totalAccountBalance: 0, + totalInvestment: 0, + totalInvestmentValueWithCurrencyEffect: 0, + value: 0, + valueWithCurrencyEffect: 0 + }); + + expect(portfolioSnapshot).toMatchObject({ + currentValueInBaseCurrency: new Big('0'), + errors: [], + hasErrors: false, + positions: [ + { + activitiesCount: 2, + averagePrice: new Big('0'), + currency: 'CHF', + dataSource: 'YAHOO', + dateOfFirstActivity: '2022-03-07', + dividend: new Big('0'), + dividendInBaseCurrency: new Big('0'), + fee: new Big('0'), + feeInBaseCurrency: new Big('0'), + grossPerformance: new Big('19.86'), + grossPerformancePercentage: new Big('0.13100263852242744063'), + grossPerformancePercentageWithCurrencyEffect: new Big( + '0.13100263852242744063' + ), + grossPerformanceWithCurrencyEffect: new Big('19.86'), + investment: new Big('0'), + investmentWithCurrencyEffect: new Big('0'), + netPerformance: new Big('19.86'), + netPerformancePercentage: new Big('0.13100263852242744063'), + netPerformancePercentageWithCurrencyEffectMap: { + max: new Big('0.13100263852242744063') + }, + netPerformanceWithCurrencyEffectMap: { + max: new Big('19.86') + }, + marketPrice: 87.8, + marketPriceInBaseCurrency: 87.8, + quantity: new Big('0'), + symbol: 'NOVN.SW', + tags: [], + timeWeightedInvestment: new Big('151.6'), + timeWeightedInvestmentWithCurrencyEffect: new Big('151.6'), + valueInBaseCurrency: new Big('0') + } + ], + totalFeesWithCurrencyEffect: new Big('0'), + totalInterestWithCurrencyEffect: new Big('0'), + totalInvestment: new Big('0'), + totalInvestmentWithCurrencyEffect: new Big('0'), + totalLiabilitiesWithCurrencyEffect: new Big('0') + }); + + expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject( + expect.objectContaining({ + netPerformance: 19.86, + netPerformanceInPercentage: 0.13100263852242744063, + netPerformanceInPercentageWithCurrencyEffect: 0.13100263852242744063, + netPerformanceWithCurrencyEffect: 19.86, + totalInvestment: 0, + totalInvestmentValueWithCurrencyEffect: 0 + }) + ); + + expect(investments).toEqual([ + { date: '2022-03-07', investment: new Big('151.6') }, + { date: '2022-04-08', investment: new Big('0') } + ]); + + expect(investmentsByMonth).toEqual([ + { date: '2022-03-01', investment: 151.6 }, + { date: '2022-04-01', investment: -151.6 } + ]); + + expect(investmentsByYear).toEqual([ + { date: '2022-01-01', investment: 0 } + ]); + }); + }); +}); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts index 1d6cee0fa..226eaa3d8 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-valuable.spec.ts @@ -1,6 +1,6 @@ import { activityDummyData, - symbolProfileDummyData, + assetProfileDummyData, userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -85,17 +85,17 @@ describe('PortfolioCalculator', () => { const activities: Activity[] = [ { ...activityDummyData, - date: new Date('2022-01-01'), - feeInAssetProfileCurrency: 0, - feeInBaseCurrency: 0, - quantity: 1, - SymbolProfile: { - ...symbolProfileDummyData, + assetProfile: { + ...assetProfileDummyData, currency: 'USD', dataSource: 'MANUAL', name: 'Penthouse Apartment', symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde' }, + date: new Date('2022-01-01'), + feeInAssetProfileCurrency: 0, + feeInBaseCurrency: 0, + quantity: 1, type: 'BUY', unitPriceInAssetProfileCurrency: 500000 } diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts index 8d90c5bc6..becab8c04 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts +++ b/apps/api/src/app/portfolio/interfaces/portfolio-order.interface.ts @@ -2,7 +2,7 @@ import { Activity } from '@ghostfolio/common/interfaces'; export interface PortfolioOrder extends Pick { assetProfile: Pick< - Activity['SymbolProfile'], + Activity['assetProfile'], 'assetSubClass' | 'currency' | 'dataSource' | 'name' | 'symbol' | 'userId' >; date: string; diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index 04142c92b..adddb4013 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -373,15 +373,15 @@ describe('PortfolioService', () => { { account, accountId: account.id, + assetProfile: { symbol: 'AAPL' }, quantity: 1, - SymbolProfile: { symbol: 'AAPL' }, type: 'BUY' }, { account: null, accountId: null, + assetProfile: { symbol: 'BABA' }, quantity: 2, - SymbolProfile: { symbol: 'BABA' }, type: 'BUY' } ], @@ -409,8 +409,8 @@ describe('PortfolioService', () => { { account, accountId: account.id, + assetProfile: { symbol: 'AAPL' }, quantity: 1, - SymbolProfile: { symbol: 'AAPL' }, type: 'BUY' } ], diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index e75e54890..6617b8f9b 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -829,10 +829,9 @@ export class PortfolioService { timeWeightedInvestmentWithCurrencyEffect } = holding; - const activitiesOfHolding = activities.filter(({ SymbolProfile }) => { + const activitiesOfHolding = activities.filter(({ assetProfile }) => { return ( - SymbolProfile.dataSource === dataSource && - SymbolProfile.symbol === symbol + assetProfile.dataSource === dataSource && assetProfile.symbol === symbol ); }); @@ -2056,11 +2055,11 @@ export class PortfolioService { .filter(({ isDraft, type }) => { return isDraft === false && type === activityType; }) - .map(({ currency, quantity, SymbolProfile, unitPrice }) => { + .map(({ assetProfile, currency, quantity, unitPrice }) => { return new Big( this.exchangeRateDataService.toCurrency( new Big(quantity).mul(unitPrice).toNumber(), - currency ?? SymbolProfile.currency, + currency ?? assetProfile.currency, userCurrency ) ); @@ -2198,16 +2197,11 @@ export class PortfolioService { } } - for (const { - account, - quantity, - SymbolProfile, - type - } of ordersByAccount) { + for (const { account, assetProfile, quantity, type } of ordersByAccount) { const currentValueOfSymbolInBaseCurrency = getFactor(type) * quantity * - (portfolioItemsNow[SymbolProfile.symbol]?.marketPriceInBaseCurrency ?? + (portfolioItemsNow[assetProfile.symbol]?.marketPriceInBaseCurrency ?? 0); if (accounts[account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) { diff --git a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts index 56d260d4d..4e75ee9b7 100644 --- a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts @@ -78,8 +78,12 @@ export class TransformDataSourceInResponseInterceptor< valueMap, object: data, paths: [ + 'activities[*].assetProfile.dataSource', 'activities[*].dataSource', + + /* @deprecated */ 'activities[*].SymbolProfile.dataSource', + 'assetProfile.dataSource', 'benchmarks[*].dataSource', 'errors[*].dataSource', @@ -88,7 +92,10 @@ export class TransformDataSourceInResponseInterceptor< 'holdings[*].assetProfile.dataSource', 'holdings[*].dataSource', 'items[*].dataSource', + + /* @deprecated */ 'SymbolProfile.dataSource', + 'watchlist[*].dataSource' ] }); diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index 397fdce57..3d1c0ceeb 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -409,10 +409,10 @@ export class GfActivitiesPageComponent implements OnInit { activity: { ...aActivity, accountId: aActivity?.accountId, + assetProfile: null, date: new Date(), id: null, fee: 0, - SymbolProfile: null, type: aActivity?.type ?? 'BUY', unitPrice: null }, diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index 022a8f2a2..be9ac7b04 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -119,7 +119,7 @@ export class GfCreateOrUpdateActivityDialogComponent { } public ngOnInit() { - this.currencyOfAssetProfile = this.data.activity?.SymbolProfile?.currency; + this.currencyOfAssetProfile = this.data.activity?.assetProfile?.currency; this.hasPermissionToCreateOwnTag = this.data.user?.settings?.isExperimentalFeatures && hasPermission(this.data.user?.permissions, permissions.createOwnTag); @@ -185,31 +185,31 @@ export class GfCreateOrUpdateActivityDialogComponent { ? this.data.accounts[0].id : this.data.activity?.accountId ], - assetClass: [this.data.activity?.SymbolProfile?.assetClass], - assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass], + assetClass: [this.data.activity?.assetProfile?.assetClass], + assetSubClass: [this.data.activity?.assetProfile?.assetSubClass], comment: [this.data.activity?.comment], currency: [ - this.data.activity?.SymbolProfile?.currency, + this.data.activity?.assetProfile?.currency, Validators.required ], currencyOfUnitPrice: [ this.data.activity?.currency ?? - this.data.activity?.SymbolProfile?.currency, + this.data.activity?.assetProfile?.currency, Validators.required ], dataSource: [ - this.data.activity?.SymbolProfile?.dataSource, + this.data.activity?.assetProfile?.dataSource, Validators.required ], date: [this.data.activity?.date, Validators.required], fee: [this.data.activity?.fee, Validators.required], - name: [this.data.activity?.SymbolProfile?.name, Validators.required], + name: [this.data.activity?.assetProfile?.name, Validators.required], quantity: [this.data.activity?.quantity, Validators.required], searchSymbol: [ - this.data.activity?.SymbolProfile + this.data.activity?.assetProfile ? { - dataSource: this.data.activity?.SymbolProfile?.dataSource, - symbol: this.data.activity?.SymbolProfile?.symbol + dataSource: this.data.activity?.assetProfile?.dataSource, + symbol: this.data.activity?.assetProfile?.symbol } : null, Validators.required @@ -310,7 +310,7 @@ export class GfCreateOrUpdateActivityDialogComponent { this.activityForm.get('searchSymbol')?.valueChanges.subscribe(() => { if (this.activityForm.get('searchSymbol')?.invalid) { - this.data.activity.SymbolProfile = null; + this.data.activity.assetProfile = null; } else if ( ['BUY', 'DIVIDEND', 'SELL'].includes( this.activityForm.get('type')?.value @@ -454,11 +454,11 @@ export class GfCreateOrUpdateActivityDialogComponent { this.activityForm.get('type')?.disable(); } - if (this.data.activity?.SymbolProfile?.symbol) { + if (this.data.activity?.assetProfile?.symbol) { this.dataService .fetchSymbolItem({ - dataSource: this.data.activity?.SymbolProfile?.dataSource, - symbol: this.data.activity?.SymbolProfile?.symbol + dataSource: this.data.activity?.assetProfile?.dataSource, + symbol: this.data.activity?.assetProfile?.symbol }) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ marketPrice }) => { @@ -569,7 +569,7 @@ export class GfCreateOrUpdateActivityDialogComponent { }) .pipe( catchError(() => { - this.data.activity.SymbolProfile = null; + this.data.activity.assetProfile = null; this.isLoading = false; diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts index 37faff91f..a3d58f8eb 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/interfaces/interfaces.ts @@ -4,9 +4,9 @@ import { Account } from '@prisma/client'; export interface CreateOrUpdateActivityDialogParams { accounts: Account[]; - activity: Partial> & { + activity: Partial> & { + assetProfile: Activity['assetProfile'] | null; id: string | null; - SymbolProfile: Activity['SymbolProfile'] | null; unitPrice: number | null; }; user: User; diff --git a/apps/client/src/app/services/import-activities.service.ts b/apps/client/src/app/services/import-activities.service.ts index f5cc74106..ff5679718 100644 --- a/apps/client/src/app/services/import-activities.service.ts +++ b/apps/client/src/app/services/import-activities.service.ts @@ -165,12 +165,12 @@ export class ImportActivitiesService { private convertToCreateOrderDto({ accountId, + assetProfile, comment, currency, date, fee, quantity, - SymbolProfile, tags, type, unitPrice, @@ -184,10 +184,10 @@ export class ImportActivitiesService { updateAccountBalance, accountId: accountId ?? undefined, comment: comment ?? undefined, - currency: currency ?? SymbolProfile.currency ?? '', - dataSource: SymbolProfile.dataSource, + currency: currency ?? assetProfile.currency ?? '', + dataSource: assetProfile.dataSource, date: date.toString(), - symbol: SymbolProfile.symbol, + symbol: assetProfile.symbol, tags: tags?.map(({ id }) => { return id; }) diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 633e13f24..e071a72c5 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -106,13 +106,20 @@ export const DEFAULT_REDACTED_PATHS = [ 'accounts[*].valueInBaseCurrency', 'activities[*].account.balance', 'activities[*].account.comment', + 'activities[*].assetProfile.symbolMapping', + 'activities[*].assetProfile.watchedByCount', 'activities[*].comment', 'activities[*].fee', 'activities[*].feeInAssetProfileCurrency', 'activities[*].feeInBaseCurrency', 'activities[*].quantity', + + /* @deprecated */ 'activities[*].SymbolProfile.symbolMapping', + + /* @deprecated */ 'activities[*].SymbolProfile.watchedByCount', + 'activities[*].value', 'activities[*].valueInBaseCurrency', 'balance', @@ -143,8 +150,13 @@ export const DEFAULT_REDACTED_PATHS = [ 'platforms[*].balance', 'platforms[*].valueInBaseCurrency', 'quantity', + + /* @deprecated */ 'SymbolProfile.symbolMapping', + + /* @deprecated */ 'SymbolProfile.watchedByCount', + 'totalBalanceInBaseCurrency', 'totalDividendInBaseCurrency', 'totalInterestInBaseCurrency', diff --git a/libs/common/src/lib/interfaces/activities.interface.ts b/libs/common/src/lib/interfaces/activities.interface.ts index b9e64984b..1cc867d6e 100644 --- a/libs/common/src/lib/interfaces/activities.interface.ts +++ b/libs/common/src/lib/interfaces/activities.interface.ts @@ -5,10 +5,16 @@ import { Order, Tag } from '@prisma/client'; export interface Activity extends Order { account?: AccountWithPlatform; + assetProfile: EnhancedSymbolProfile; error?: ActivityError; feeInAssetProfileCurrency: number; feeInBaseCurrency: number; - SymbolProfile: EnhancedSymbolProfile; + + /** + * @deprecated Use `assetProfile` instead + */ + SymbolProfile?: EnhancedSymbolProfile; + tagIds?: string[]; tags?: Tag[]; unitPriceInAssetProfileCurrency: number; diff --git a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts index 18c7dc57a..04c8e0c07 100644 --- a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts @@ -26,7 +26,13 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { Order, 'currency' | 'date' | 'fee' | 'quantity' | 'type' | 'unitPrice' > & { + assetProfile?: EnhancedSymbolProfile; + + /** + * @deprecated Use `assetProfile` instead + */ SymbolProfile?: EnhancedSymbolProfile; + value: number; valueInBaseCurrency: number; })[]; diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index a98d51f56..172059d1c 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -149,9 +149,9 @@ @@ -162,18 +162,18 @@
- {{ element.SymbolProfile?.name }} + {{ element.assetProfile?.name }} @if (element.isDraft) { Draft }
@if ( - element.SymbolProfile?.dataSource !== 'MANUAL' && - !isUUID(element.SymbolProfile?.symbol) + element.assetProfile?.dataSource !== 'MANUAL' && + !isUUID(element.assetProfile?.symbol) ) {
{{ - element.SymbolProfile?.symbol | gfSymbol + element.assetProfile?.symbol | gfSymbol }}
} @@ -309,7 +309,7 @@ class="d-none d-lg-table-cell px-1" mat-cell > - {{ element.currency ?? element.SymbolProfile?.currency }} + {{ element.currency ?? element.assetProfile?.currency }} diff --git a/libs/ui/src/lib/activities-table/activities-table.component.stories.ts b/libs/ui/src/lib/activities-table/activities-table.component.stories.ts index 25463e576..957de2262 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.stories.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.stories.ts @@ -56,7 +56,7 @@ const activities: Activity[] = [ url: 'https://interactivebrokers.com' } }, - SymbolProfile: { + assetProfile: { assetClass: 'EQUITY', assetSubClass: 'ETF', comment: undefined, @@ -123,7 +123,7 @@ const activities: Activity[] = [ url: 'https://interactivebrokers.com' } }, - SymbolProfile: { + assetProfile: { assetClass: 'EQUITY', assetSubClass: 'ETF', comment: undefined, @@ -190,7 +190,7 @@ const activities: Activity[] = [ url: 'https://interactivebrokers.com' } }, - SymbolProfile: { + assetProfile: { assetClass: 'LIQUIDITY', assetSubClass: 'CRYPTOCURRENCY', comment: undefined, @@ -257,7 +257,7 @@ const activities: Activity[] = [ url: 'https://interactivebrokers.com' } }, - SymbolProfile: { + assetProfile: { assetClass: 'FIXED_INCOME', assetSubClass: 'BOND', comment: 'No data', @@ -324,7 +324,7 @@ const activities: Activity[] = [ url: 'https://interactivebrokers.com' } }, - SymbolProfile: { + assetProfile: { assetClass: 'EQUITY', assetSubClass: 'ETF', comment: undefined, diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts index dd792ca35..b1bfcf686 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -285,8 +285,8 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit { } } else if (this.canClickActivity(activity)) { this.activityClicked.emit({ - dataSource: activity.SymbolProfile.dataSource, - symbol: activity.SymbolProfile.symbol + dataSource: activity.assetProfile.dataSource, + symbol: activity.assetProfile.symbol }); } } From df6fa86b1d7ef19d818f9d258f442ea3fd8c0c83 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:22:06 +0200 Subject: [PATCH 5/9] Task/harden validation of holdings in asset profile endpoints (#7320) * Harden validation of holdings * Update changelog --- CHANGELOG.md | 1 + libs/common/src/lib/dtos/create-asset-profile.dto.ts | 3 +++ libs/common/src/lib/dtos/holding.dto.ts | 11 +++++++++++ libs/common/src/lib/dtos/index.ts | 2 ++ libs/common/src/lib/dtos/update-asset-profile.dto.ts | 3 +++ 5 files changed, 20 insertions(+) create mode 100644 libs/common/src/lib/dtos/holding.dto.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9011a3350..1106df29b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Hardened the validation of the countries in the asset profile endpoints +- Hardened the validation of the holdings in the asset profile endpoints - Hardened the validation of the sectors in the asset profile endpoints - Rounded the value of the _Fear & Greed Index_ (market mood) in the twitter bot service - Set the change detection strategy to `OnPush` in the _X-ray_ page diff --git a/libs/common/src/lib/dtos/create-asset-profile.dto.ts b/libs/common/src/lib/dtos/create-asset-profile.dto.ts index 0b0ccb9d3..17a9d12b9 100644 --- a/libs/common/src/lib/dtos/create-asset-profile.dto.ts +++ b/libs/common/src/lib/dtos/create-asset-profile.dto.ts @@ -13,6 +13,7 @@ import { } from 'class-validator'; import { CountryDto } from './country.dto'; +import { HoldingDto } from './holding.dto'; import { SectorDto } from './sector.dto'; export class CreateAssetProfileDto { @@ -58,6 +59,8 @@ export class CreateAssetProfileDto { @IsArray() @IsOptional() + @Type(() => HoldingDto) + @ValidateNested({ each: true }) holdings?: Prisma.InputJsonArray; @IsBoolean() diff --git a/libs/common/src/lib/dtos/holding.dto.ts b/libs/common/src/lib/dtos/holding.dto.ts new file mode 100644 index 000000000..8c093e3bc --- /dev/null +++ b/libs/common/src/lib/dtos/holding.dto.ts @@ -0,0 +1,11 @@ +import { IsNumber, IsString, Max, Min } from 'class-validator'; + +export class HoldingDto { + @IsNumber() + @Max(1) + @Min(0) + allocationInPercentage: number; + + @IsString() + name: string; +} diff --git a/libs/common/src/lib/dtos/index.ts b/libs/common/src/lib/dtos/index.ts index c74c68dfd..c531313a6 100644 --- a/libs/common/src/lib/dtos/index.ts +++ b/libs/common/src/lib/dtos/index.ts @@ -11,6 +11,7 @@ import { CreatePlatformDto } from './create-platform.dto'; import { CreateTagDto } from './create-tag.dto'; import { CreateWatchlistItemDto } from './create-watchlist-item.dto'; import { DeleteOwnUserDto } from './delete-own-user.dto'; +import { HoldingDto } from './holding.dto'; import { SectorDto } from './sector.dto'; import { TransferBalanceDto } from './transfer-balance.dto'; import { UpdateAccessDto } from './update-access.dto'; @@ -40,6 +41,7 @@ export { CreateTagDto, CreateWatchlistItemDto, DeleteOwnUserDto, + HoldingDto, SectorDto, TransferBalanceDto, UpdateAccessDto, diff --git a/libs/common/src/lib/dtos/update-asset-profile.dto.ts b/libs/common/src/lib/dtos/update-asset-profile.dto.ts index 5b193ff7e..2204fd0ab 100644 --- a/libs/common/src/lib/dtos/update-asset-profile.dto.ts +++ b/libs/common/src/lib/dtos/update-asset-profile.dto.ts @@ -20,6 +20,7 @@ import { } from 'class-validator'; import { CountryDto } from './country.dto'; +import { HoldingDto } from './holding.dto'; import { SectorDto } from './sector.dto'; export class UpdateAssetProfileDto { @@ -55,6 +56,8 @@ export class UpdateAssetProfileDto { @IsArray() @IsOptional() + @Type(() => HoldingDto) + @ValidateNested({ each: true }) holdings?: Prisma.InputJsonArray; @IsBoolean() From a2c28bd4a04854000c43b5b07eced754115bd5e7 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 13 Jul 2026 20:24:07 +0200 Subject: [PATCH 6/9] Task/upgrade Nx to version 23.0.2 (#7321) * Upgrade Nx dependencies to version 23.0.2 * Update changelog --- CHANGELOG.md | 1 + package-lock.json | 528 ++++++++++++++++++++++++++-------------------- package.json | 22 +- 3 files changed, 310 insertions(+), 241 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1106df29b..85df11fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rounded the value of the _Fear & Greed Index_ (market mood) in the twitter bot service - Set the change detection strategy to `OnPush` in the _X-ray_ page - Deprecated `SymbolProfile` in favor of `assetProfile` in the activity interface +- Upgraded `Nx` from version `23.0.1` to `23.0.2` ## 3.25.0 - 2026-07-12 diff --git a/package-lock.json b/package-lock.json index 4c0b837ee..ca2585f35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -117,16 +117,16 @@ "@eslint/js": "9.35.0", "@nestjs/schematics": "11.1.0", "@nestjs/testing": "11.1.27", - "@nx/angular": "23.0.1", - "@nx/eslint-plugin": "23.0.1", - "@nx/jest": "23.0.1", - "@nx/js": "23.0.1", - "@nx/module-federation": "23.0.1", - "@nx/nest": "23.0.1", - "@nx/node": "23.0.1", - "@nx/storybook": "23.0.1", - "@nx/web": "23.0.1", - "@nx/workspace": "23.0.1", + "@nx/angular": "23.0.2", + "@nx/eslint-plugin": "23.0.2", + "@nx/jest": "23.0.2", + "@nx/js": "23.0.2", + "@nx/module-federation": "23.0.2", + "@nx/nest": "23.0.2", + "@nx/node": "23.0.2", + "@nx/storybook": "23.0.2", + "@nx/web": "23.0.2", + "@nx/workspace": "23.0.2", "@prisma/config": "7.8.0", "@schematics/angular": "21.2.6", "@storybook/addon-docs": "10.1.10", @@ -154,7 +154,7 @@ "jest": "30.3.0", "jest-environment-jsdom": "30.2.0", "jest-preset-angular": "16.0.0", - "nx": "23.0.1", + "nx": "23.0.2", "prettier": "3.8.4", "prettier-plugin-organize-attributes": "1.0.0", "prisma": "7.8.0", @@ -7835,19 +7835,19 @@ } }, "node_modules/@nx/angular": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/angular/-/angular-23.0.1.tgz", - "integrity": "sha512-/SJEhwGrAOO35fQjRMwLi3RBqzSaPNViCGqiQldeTokzJqtr9h4QVQxVH+mD+zLdwRI4teoC9taekVrsjXcMoA==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/angular/-/angular-23.0.2.tgz", + "integrity": "sha512-2xeTEyMgeUMLN9YFSnlQj38JxGjkOUmkSKvRCcrTJNZFuZpxMxwW/oXkPJ3EBBjryAMWCA/qyAh3Te0dokw0/g==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "23.0.1", - "@nx/eslint": "23.0.1", - "@nx/js": "23.0.1", - "@nx/module-federation": "23.0.1", - "@nx/rspack": "23.0.1", - "@nx/web": "23.0.1", - "@nx/webpack": "23.0.1", + "@nx/devkit": "23.0.2", + "@nx/eslint": "23.0.2", + "@nx/js": "23.0.2", + "@nx/module-federation": "23.0.2", + "@nx/rspack": "23.0.2", + "@nx/web": "23.0.2", + "@nx/webpack": "23.0.2", "@phenomnomnominal/tsquery": "~6.2.0", "@typescript-eslint/type-utils": "^8.0.0", "enquirer": "~2.3.6", @@ -7895,15 +7895,15 @@ } }, "node_modules/@nx/cypress": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/cypress/-/cypress-23.0.1.tgz", - "integrity": "sha512-is3oXDoTd2K4C7b2qq3tzKVctBDOdKsfEHIztCwV7ZO3jd9cWl83fLQUL6WI3WVoafo8ALxARRSe1ri4GkzyFg==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/cypress/-/cypress-23.0.2.tgz", + "integrity": "sha512-+6RbjMA4KHcBNFIbqOF6oKDWwGsqsJaf2uhH69vDM9rfjJnL7j5CKFzLGSO746Nk4BcLqI0ARR6R8T+w0Ees5g==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "23.0.1", - "@nx/eslint": "23.0.1", - "@nx/js": "23.0.1", + "@nx/devkit": "23.0.2", + "@nx/eslint": "23.0.2", + "@nx/js": "23.0.2", "@phenomnomnominal/tsquery": "~6.2.0", "detect-port": "^2.1.0", "semver": "^7.6.3", @@ -7920,9 +7920,9 @@ } }, "node_modules/@nx/devkit": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-23.0.1.tgz", - "integrity": "sha512-A/chuNS1RZwdbRe/Nf+w0qtPEFHLcZNPzo8Abw5mBxyXmy9yvHZpuZuqDbt/lASFU+TEb74xExL1AnKWwqpOIg==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-23.0.2.tgz", + "integrity": "sha512-svkaufC+nucp3stMucqVN0sV4nz9fEgAd33WB9u2fnFWYx/4F0VJcV4ZLyiG3hHrgs2/Gz5K0OnygPqjCsSqMw==", "dev": true, "license": "MIT", "dependencies": { @@ -7932,6 +7932,7 @@ "minimatch": "10.2.5", "semver": "^7.6.3", "tslib": "^2.3.0", + "yaml": "^2.8.3", "yargs-parser": "21.1.1" }, "peerDependencies": { @@ -7949,9 +7950,9 @@ } }, "node_modules/@nx/devkit/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { @@ -7991,32 +7992,32 @@ } }, "node_modules/@nx/docker": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/docker/-/docker-23.0.1.tgz", - "integrity": "sha512-H2/wGZa10X2KhM+BzUkZLCI9wDG1kGqDGco1tDWZczKwk9ViOcwV23ljjt5RtIQIson6B7L56pO2F/8WKJ+1CQ==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/docker/-/docker-23.0.2.tgz", + "integrity": "sha512-3eccPjL3kwAndVq7t0V1XQDEkEGz7AhnSzpPwr9IMHgVKqgCIJBbNF1Xi5c3AbPlnjy0GHfnGOeb1/jXf2RuNA==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "23.0.1", + "@nx/devkit": "23.0.2", "enquirer": "~2.3.6", "tslib": "^2.3.0" } }, "node_modules/@nx/eslint": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/eslint/-/eslint-23.0.1.tgz", - "integrity": "sha512-/P+iXDUsXHeqU7NMDviE/tjJkaVWssc7TLcCoJ6TRy/p+U7HaigLx57VAogBIznldHiyL7KbM7Y0fzqo/hPofw==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/eslint/-/eslint-23.0.2.tgz", + "integrity": "sha512-nghKUrUWPj6mZajGkPIC1LxSO+GYWifZjtFnEQU7n5SDffS+QA3mUX5Tr16CZL5tcSLTwpk2/fx3GM/Ji/3DOA==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "23.0.1", - "@nx/js": "23.0.1", + "@nx/devkit": "23.0.2", + "@nx/js": "23.0.2", "semver": "^7.6.3", "tslib": "^2.3.0", "typescript": "~5.9.2" }, "peerDependencies": { - "@nx/jest": "23.0.1", + "@nx/jest": "23.0.2", "@zkochan/js-yaml": "0.0.7", "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0" }, @@ -8030,14 +8031,14 @@ } }, "node_modules/@nx/eslint-plugin": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/eslint-plugin/-/eslint-plugin-23.0.1.tgz", - "integrity": "sha512-fZ4fU4fYxvmHYUtiPuh9vg6KItjqaz4c8SHeqOCRvDsbm7NwQidWsGiruk853No5KnmCHfj/En87v/qGOguoGw==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/eslint-plugin/-/eslint-plugin-23.0.2.tgz", + "integrity": "sha512-ckkifbXqmYrA7OgpfxqeqG5feYndN28hfvbMA0yD0nWQFbx24lC6CkzsJJjuIN3RsQa8yrhKIoObYtiMRxdNLQ==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "23.0.1", - "@nx/js": "23.0.1", + "@nx/devkit": "23.0.2", + "@nx/js": "23.0.2", "@phenomnomnominal/tsquery": "~6.2.0", "@typescript-eslint/type-utils": "^8.0.0", "@typescript-eslint/utils": "^8.0.0", @@ -8075,16 +8076,16 @@ } }, "node_modules/@nx/jest": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/jest/-/jest-23.0.1.tgz", - "integrity": "sha512-F5lhjttIExH8hEJ09cqv3Ac1o8GyGTHpD1OLLWnNAYaN/CPpbI16Ix8VLY/fuwJYf6wcm1/vBL3knbv6LABODA==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/jest/-/jest-23.0.2.tgz", + "integrity": "sha512-NGMUJMzEccj0y/yk+ct+OIFNB1RrdC9C6rFBZ3VFJgxbJlSbxCqQXcO+/668CtGlLVPAZe0baASrCVcvKNUGCQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/reporters": "^30.0.2", "@jest/test-result": "^30.0.2", - "@nx/devkit": "23.0.1", - "@nx/js": "23.0.1", + "@nx/devkit": "23.0.2", + "@nx/js": "23.0.2", "@phenomnomnominal/tsquery": "~6.2.0", "identity-obj-proxy": "3.0.0", "jest-config": "^30.0.2", @@ -8150,9 +8151,9 @@ } }, "node_modules/@nx/js": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/js/-/js-23.0.1.tgz", - "integrity": "sha512-H8jw1gk7hA8PCXBFC9ocTBpzuXOTvVQ1gA+OlEBMyKqmUaOLNm7yuoOYozwvLsLlCVY27onohSIS8xIdAR/Zow==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/js/-/js-23.0.2.tgz", + "integrity": "sha512-ixtH09vcr6qFaYloCC8JOMLazxTocws2FnHWNbLOpYaO0Nyh4szh8Ttclky7eiI21XYF9z07+0O7/GFlh8hCBA==", "dev": true, "license": "MIT", "dependencies": { @@ -8163,8 +8164,8 @@ "@babel/preset-env": "^7.23.2", "@babel/preset-typescript": "^7.22.5", "@babel/runtime": "^7.22.6", - "@nx/devkit": "23.0.1", - "@nx/workspace": "23.0.1", + "@nx/devkit": "23.0.2", + "@nx/workspace": "23.0.2", "@zkochan/js-yaml": "0.0.7", "babel-plugin-const-enum": "^1.0.1", "babel-plugin-macros": "^3.1.0", @@ -8228,15 +8229,15 @@ } }, "node_modules/@nx/module-federation": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/module-federation/-/module-federation-23.0.1.tgz", - "integrity": "sha512-scYruTqvCegeTwAHiBL9MNRKb9RaU+XJ3+wxcmZekyNnvcQjmEPPHndE013wHUuwohyEpVFYrVCulbVb+h2Tvg==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/module-federation/-/module-federation-23.0.2.tgz", + "integrity": "sha512-CkSmu0FCb85YU2mNmi96a5csZzXoas9NJZSj5WEvwOWJpfasImpqMrD06ozG27vv6b/TbIPC1MXUfQw5eyxP5g==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "23.0.1", - "@nx/js": "23.0.1", - "@nx/web": "23.0.1", + "@nx/devkit": "23.0.2", + "@nx/js": "23.0.2", + "@nx/web": "23.0.2", "@rspack/core": "1.6.8", "express": "^4.21.2", "http-proxy-middleware": "^3.0.5", @@ -8314,9 +8315,9 @@ } }, "node_modules/@nx/module-federation/node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "dev": true, "license": "MIT", "dependencies": { @@ -8596,17 +8597,17 @@ } }, "node_modules/@nx/nest": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/nest/-/nest-23.0.1.tgz", - "integrity": "sha512-/iSE/XzbOl1C1zDuQgeoKBmQAIJYwAj1PJNrnJKf2fS8Mf1s2wGdWnegNsp7iNmAWvZw3zXxhozFiNzJv/HUzA==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/nest/-/nest-23.0.2.tgz", + "integrity": "sha512-HwawKHpIImnwNfLyULeE+AywB/BIVm9m+ZuoulYP8Gwjvg7Lrq28stnf98sTKEuyGjDDWvTWH4RNHYs0nRXEiA==", "dev": true, "license": "MIT", "dependencies": { "@nestjs/schematics": "^11.0.0", - "@nx/devkit": "23.0.1", - "@nx/eslint": "23.0.1", - "@nx/js": "23.0.1", - "@nx/node": "23.0.1", + "@nx/devkit": "23.0.2", + "@nx/eslint": "23.0.2", + "@nx/js": "23.0.2", + "@nx/node": "23.0.2", "semver": "^7.6.3", "tslib": "^2.3.0" }, @@ -8632,17 +8633,17 @@ } }, "node_modules/@nx/node": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/node/-/node-23.0.1.tgz", - "integrity": "sha512-s13ja7MITncR4M/uTGM786Jqco8lJs/9Kvo7jrKFg0Ct8d5XHbu+JFpulmCGPatsBhFA0E/JHSSfR+D9E7MWxA==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/node/-/node-23.0.2.tgz", + "integrity": "sha512-WugI9xFXFs0GEKaelYDl+kpbPgwtvleSI1gnCxsA8MukjqZlm5cSTB4/+fOQSFaWhTesR0BZW5SasRQBJTB65A==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "23.0.1", - "@nx/docker": "23.0.1", - "@nx/eslint": "23.0.1", - "@nx/jest": "23.0.1", - "@nx/js": "23.0.1", + "@nx/devkit": "23.0.2", + "@nx/docker": "23.0.2", + "@nx/eslint": "23.0.2", + "@nx/jest": "23.0.2", + "@nx/js": "23.0.2", "kill-port": "^1.6.1", "semver": "^7.6.3", "tcp-port-used": "^1.0.2", @@ -8666,9 +8667,9 @@ } }, "node_modules/@nx/nx-darwin-arm64": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-23.0.1.tgz", - "integrity": "sha512-gQJvgPnbI91DBe23Th2CqD9R/S54cPS3C1f0DhyQ8YEf9rR7EEc+sVGjhgVxlhfOk2W7I1Gy6EkXwpN4aDoW4w==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-23.0.2.tgz", + "integrity": "sha512-9sqhZMVFpF+qM7hq6y2xA4gVK+6RdxRioAwHxorhOZRSXdW7Y7NESs5fm8vOmdddlG07QB7sMefOKLrqCV3zGg==", "cpu": [ "arm64" ], @@ -8680,9 +8681,9 @@ ] }, "node_modules/@nx/nx-darwin-x64": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-23.0.1.tgz", - "integrity": "sha512-e/lvzHKN6gpuD7MqEtfH1fOfnR75E55ytYNt8jaRxKI6EvpCq+Q3MunDuh9GQYAkqDrUqE7AhHrHc+eKATVEHw==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-23.0.2.tgz", + "integrity": "sha512-p6L3AvRhRRaR8Bl3jr76/9H04RdWUQbSgB7agK7GB7vqaLI8RifP2lqeaXcAngzjDAjw2EAf0TjOBP+T67hhcg==", "cpu": [ "x64" ], @@ -8694,9 +8695,9 @@ ] }, "node_modules/@nx/nx-freebsd-x64": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-23.0.1.tgz", - "integrity": "sha512-f582OhSYN9qHpA9Ox9qnr3kZSZ7gQHs7crmBUutmbXmZQB2TDS/TlhvYSNnxudpwHR/tuWGi2IOQqa7zGOZj1Q==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-23.0.2.tgz", + "integrity": "sha512-/py4I8Rp2UURses9H/+SQmgPVnHVSJgPimJLhXIfsRavKGu4RS7Ddu1OyNqSkCT3Otic6ImMTtkufURW22KiEQ==", "cpu": [ "x64" ], @@ -8708,9 +8709,9 @@ ] }, "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-23.0.1.tgz", - "integrity": "sha512-VjhqPc6E7aiI0e+lowrkVbdyulsmP9fgMdcX1mCzXCEu/XZDcUbZ5qveR964cMhvm5qKn0ILJtJOUqZgmOT3Xg==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-23.0.2.tgz", + "integrity": "sha512-xv2IzeiWJFWi4WjK0ocMkP+ze1lDeoPVCg0xOTqVs40gM66V1wVw3EK077gTqU4m0Bq1wUxe6/I8WaIGlkLgug==", "cpu": [ "arm" ], @@ -8722,9 +8723,9 @@ ] }, "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-23.0.1.tgz", - "integrity": "sha512-zX2JdHQejZWB3DRgNsh77qOVYaSSjSLuBP2qIqc7EWVlCUnR7Aj3e65PTIps4LxMMmUp4twZA2ezS0rtyK2A4w==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-23.0.2.tgz", + "integrity": "sha512-ckA6hTXST+agxt/HzPGqMss9qFCZhO9b07o8usygb7QFBYQRXFgcYzhTblq4yiTL5ibJmXAGGh98011fLA2MVw==", "cpu": [ "arm64" ], @@ -8736,9 +8737,9 @@ ] }, "node_modules/@nx/nx-linux-arm64-musl": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-23.0.1.tgz", - "integrity": "sha512-9lyhxRNBgNYwHt6paq0OLzoKNoEGF5LnNW2YYrgFY8Cjtsg/Q4pcfZ1vB5o9FX9OmUgUQs3t2d4tU8YDukRUWg==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-23.0.2.tgz", + "integrity": "sha512-PAxBxy7m//cKxUeIb6Sk2X5MJ/wjcJcqCx7/L0p8omTt/y/+q1TGpVy6qmJMPUWzNgAULXtsVRSOK4rmiBcrQQ==", "cpu": [ "arm64" ], @@ -8750,9 +8751,9 @@ ] }, "node_modules/@nx/nx-linux-x64-gnu": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-23.0.1.tgz", - "integrity": "sha512-kVszY2xRyyrCXgdCdM1qG1WUhDjNPZxtdWq86a0TyIRJjfJTP9NHqpyhmvj9c2RdZxKVWHotx6fBJzY6Vn2ZrA==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-23.0.2.tgz", + "integrity": "sha512-gR146Mo+BjhrIU2fNOJeVjX8HEAHrtmc7IyrD0qD9yqyq0l9Mdx92JnMW3yVQaYIMpPJIqsOvHTDIUbTLFrmTA==", "cpu": [ "x64" ], @@ -8764,9 +8765,9 @@ ] }, "node_modules/@nx/nx-linux-x64-musl": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-23.0.1.tgz", - "integrity": "sha512-co71K2n4zcS1SYR8EBRlCvIko7M1YycO2tZL0nrCrga87AF5dzCwx+wEclpyCR/4tNOY3FrACk4gIkVskh3CdA==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-23.0.2.tgz", + "integrity": "sha512-L7JkaoAI0p+DpAi2CNCqPq8nHWkApQw2td0Cyt+stOx/OJlmEteyc/MXyTHBoMPoopNj1wWANJJQm/G1anOLhQ==", "cpu": [ "x64" ], @@ -8778,9 +8779,9 @@ ] }, "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-23.0.1.tgz", - "integrity": "sha512-7oma7iy5fbnn+x5AP7SFGMuleAA2R5RZm26dn+faikyQ4PXjoRAikWJJNiOWAeCA0BaMAeVedI6fJeAsVeDUKg==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-23.0.2.tgz", + "integrity": "sha512-B3ePaYeu31ivP3i72Vou5RBYFVrDKCMZgz7Jsax+RvEJa8dNfI+Ynh/PSoXzHudN0YsrekkKbjlxNvp/D6fWFA==", "cpu": [ "arm64" ], @@ -8792,9 +8793,9 @@ ] }, "node_modules/@nx/nx-win32-x64-msvc": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-23.0.1.tgz", - "integrity": "sha512-TE/wvBa2cpkVXmk/AXUQAneong4JReS2hyNpAUONKG1yXU7TDKe0wvn1xQXxAbyspudT9NuCnVtpVuEkRz8S+Q==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-23.0.2.tgz", + "integrity": "sha512-/NiB9w8nYrw7LUkcmwAJ9wis5O+kh3ahSZXMDHVYgFnD8yN7kLql39MNJD+/DGstjNSDvWh45ftqr7mZMi6wpQ==", "cpu": [ "x64" ], @@ -8806,16 +8807,16 @@ ] }, "node_modules/@nx/rspack": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/rspack/-/rspack-23.0.1.tgz", - "integrity": "sha512-Hbn4vUrotIy4EVaPHKQX/ijuuH9+fx3ndaEdzQT0O08iJmVmABozsJ1GmQDYO4aU4qvu3eDsJr1IOQ3jqgp7vQ==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/rspack/-/rspack-23.0.2.tgz", + "integrity": "sha512-Aa/7028o5pPqFbR6qG+kurkrGx+/CfSJiOgwsDz1ND9pd1SYP/Z2J9YXL/tgxsDWMy24FaAJvs4MvHMZvAyvLw==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "23.0.1", - "@nx/js": "23.0.1", - "@nx/module-federation": "23.0.1", - "@nx/web": "23.0.1", + "@nx/devkit": "23.0.2", + "@nx/js": "23.0.2", + "@nx/module-federation": "23.0.2", + "@nx/web": "23.0.2", "@phenomnomnominal/tsquery": "~6.2.0", "autoprefixer": "^10.4.9", "browserslist": "^4.26.0", @@ -8888,9 +8889,9 @@ } }, "node_modules/@nx/rspack/node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "dev": true, "license": "MIT", "dependencies": { @@ -9228,22 +9229,22 @@ } }, "node_modules/@nx/storybook": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/storybook/-/storybook-23.0.1.tgz", - "integrity": "sha512-QNJHgxG4qwHpvpcaJ4sllpmlxHCEJjse1dTBFRGL9U+F71ye/W6vYrmHirKuV9yO1X6DR6D1rQMj6WVTGSnvzw==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/storybook/-/storybook-23.0.2.tgz", + "integrity": "sha512-UG6Vh66L6rfSicjUDyP6dkdkX/TimoXxvmHmUXfO2YZlR/uQXIbkav2KpYloea7NPyjX2jehezCEOrTbINEf6A==", "dev": true, "license": "MIT", "dependencies": { - "@nx/cypress": "23.0.1", - "@nx/devkit": "23.0.1", - "@nx/eslint": "23.0.1", - "@nx/js": "23.0.1", + "@nx/cypress": "23.0.2", + "@nx/devkit": "23.0.2", + "@nx/eslint": "23.0.2", + "@nx/js": "23.0.2", "@phenomnomnominal/tsquery": "~6.2.0", "semver": "^7.6.3", "tslib": "^2.3.0" }, "peerDependencies": { - "@nx/web": "23.0.1", + "@nx/web": "23.0.2", "storybook": ">=8.0.0 <11.0.0" }, "peerDependenciesMeta": { @@ -9253,26 +9254,26 @@ } }, "node_modules/@nx/web": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/web/-/web-23.0.1.tgz", - "integrity": "sha512-0iwqCPJ5A27/XzqF6637dcRmJO3/TY7nQrEobS0yJ4W2xhvCIRwsWmPkuUAZgjd4622yQauHAwPQAnkIEILPiQ==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/web/-/web-23.0.2.tgz", + "integrity": "sha512-PIUMDB79yvt6nj9ypLTCYZfA7N9v8mvWEHRbt43rusrzl3P4wqZIiveuyIstmt4HuvIHW3rRjbZTSs/eYrkqRg==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "23.0.1", - "@nx/js": "23.0.1", + "@nx/devkit": "23.0.2", + "@nx/js": "23.0.2", "detect-port": "^2.1.0", "http-server": "^14.1.0", "picocolors": "^1.1.0", "tslib": "^2.3.0" }, "peerDependencies": { - "@nx/cypress": "23.0.1", - "@nx/eslint": "23.0.1", - "@nx/jest": "23.0.1", - "@nx/playwright": "23.0.1", - "@nx/vite": "23.0.1", - "@nx/webpack": "23.0.1" + "@nx/cypress": "23.0.2", + "@nx/eslint": "23.0.2", + "@nx/jest": "23.0.2", + "@nx/playwright": "23.0.2", + "@nx/vite": "23.0.2", + "@nx/webpack": "23.0.2" }, "peerDependenciesMeta": { "@nx/cypress": { @@ -9296,15 +9297,15 @@ } }, "node_modules/@nx/webpack": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/webpack/-/webpack-23.0.1.tgz", - "integrity": "sha512-0ELnKItRtIlcHAN8AaSY4uwabA62aAFDCvYFqadQCTeN5eBYl1VGfzenWMvGyVZoU7K9dYtPVSkZSUhltvHQgw==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/webpack/-/webpack-23.0.2.tgz", + "integrity": "sha512-TbMBG8mLaD7SImTNKH9CZyhgciS6vJZ6/dPK01aHJ4lamKtnFyRGsktgPkLvhKH9jJqLSWSWb4IWAsoHMe46GA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.23.2", - "@nx/devkit": "23.0.1", - "@nx/js": "23.0.1", + "@nx/devkit": "23.0.2", + "@nx/js": "23.0.2", "@phenomnomnominal/tsquery": "~6.2.0", "ajv": "^8.0.0", "autoprefixer": "^10.4.9", @@ -9331,7 +9332,7 @@ "source-map-loader": "^5.0.0", "style-loader": "^3.3.0", "terser-webpack-plugin": "^5.3.3", - "ts-loader": "^9.3.1", + "ts-loader": "^9.5.7", "tsconfig-paths-webpack-plugin": "4.2.0", "tslib": "^2.3.0", "webpack-node-externals": "^3.0.0", @@ -9461,17 +9462,17 @@ "license": "MIT" }, "node_modules/@nx/workspace": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-23.0.1.tgz", - "integrity": "sha512-VdbvMTSEzp3ONZwiy83XEu8ktykC8aEI7M4mqKs5RNKHBFg3jtao2NFo3wDqHqnn1q9Fdaj8EbyUn08BUR5L3w==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-23.0.2.tgz", + "integrity": "sha512-7As52HT7l7ypwukR0cKZIDZ771O0hYTDwkfJmzWPHl3YghiROnK3/Xlh/84ITo+4gtCMORUyZXZgQfOi8136ug==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "23.0.1", + "@nx/devkit": "23.0.2", "@zkochan/js-yaml": "0.0.7", "chalk": "^4.1.0", "enquirer": "~2.3.6", - "nx": "23.0.1", + "nx": "23.0.2", "picomatch": "4.0.4", "semver": "^7.6.3", "tslib": "^2.3.0", @@ -15072,17 +15073,45 @@ } }, "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -17277,9 +17306,9 @@ } }, "node_modules/css-minimizer-webpack-plugin/node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", "dev": true, "funding": [ { @@ -26840,9 +26869,9 @@ "license": "MIT" }, "node_modules/nx": { - "version": "23.0.1", - "resolved": "https://registry.npmjs.org/nx/-/nx-23.0.1.tgz", - "integrity": "sha512-HnK0Ke8FcPeVQffYm1oyzkLNn7khrI8SeDeC3iyLhw/UEMCB24hjI5JSs6Amlyeb0/GaeiuQuts8NkQKd/NpGA==", + "version": "23.0.2", + "resolved": "https://registry.npmjs.org/nx/-/nx-23.0.2.tgz", + "integrity": "sha512-e5H6ceqj0Z8ovAmtsiHXswwpiNPrr1DRhvJMTpc2AW8G1za9PKxk3bP5josShsIrmGEsOlBNZZsxszXA2+Q2dw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -26855,12 +26884,13 @@ "@tybys/wasm-util": "0.9.0", "@yarnpkg/lockfile": "1.1.0", "@zkochan/js-yaml": "0.0.7", + "agent-base": "6.0.2", "ansi-colors": "4.1.3", "ansi-regex": "5.0.1", "ansi-styles": "4.3.0", "argparse": "2.0.1", "asynckit": "0.4.0", - "axios": "1.16.0", + "axios": "1.16.1", "balanced-match": "4.0.3", "base64-js": "1.5.1", "bl": "4.1.0", @@ -26875,6 +26905,7 @@ "color-convert": "2.0.1", "color-name": "1.1.4", "combined-stream": "1.0.8", + "debug": "4.4.3", "defaults": "1.0.4", "define-lazy-prop": "2.0.0", "delayed-stream": "1.0.0", @@ -26905,6 +26936,7 @@ "has-symbols": "1.1.0", "has-tostringtag": "1.0.2", "hasown": "2.0.4", + "https-proxy-agent": "5.0.1", "ieee754": "1.2.1", "ignore": "7.0.5", "inherits": "2.0.4", @@ -26924,6 +26956,7 @@ "mimic-fn": "2.1.0", "minimatch": "10.2.5", "minimist": "1.2.8", + "ms": "2.1.3", "npm-run-path": "4.0.1", "once": "1.4.0", "onetime": "5.1.2", @@ -26964,16 +26997,16 @@ "nx-cloud": "dist/bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "23.0.1", - "@nx/nx-darwin-x64": "23.0.1", - "@nx/nx-freebsd-x64": "23.0.1", - "@nx/nx-linux-arm-gnueabihf": "23.0.1", - "@nx/nx-linux-arm64-gnu": "23.0.1", - "@nx/nx-linux-arm64-musl": "23.0.1", - "@nx/nx-linux-x64-gnu": "23.0.1", - "@nx/nx-linux-x64-musl": "23.0.1", - "@nx/nx-win32-arm64-msvc": "23.0.1", - "@nx/nx-win32-x64-msvc": "23.0.1" + "@nx/nx-darwin-arm64": "23.0.2", + "@nx/nx-darwin-x64": "23.0.2", + "@nx/nx-freebsd-x64": "23.0.2", + "@nx/nx-linux-arm-gnueabihf": "23.0.2", + "@nx/nx-linux-arm64-gnu": "23.0.2", + "@nx/nx-linux-arm64-musl": "23.0.2", + "@nx/nx-linux-x64-gnu": "23.0.2", + "@nx/nx-linux-x64-musl": "23.0.2", + "@nx/nx-win32-arm64-msvc": "23.0.2", + "@nx/nx-win32-x64-msvc": "23.0.2" }, "peerDependencies": { "@swc-node/register": "^1.11.1", @@ -27039,6 +27072,19 @@ "tslib": "^2.4.0" } }, + "node_modules/nx/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/nx/node_modules/balanced-match": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", @@ -27128,6 +27174,20 @@ "node": ">=0.8.0" } }, + "node_modules/nx/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/nx/node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -27256,6 +27316,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/nx/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nx/node_modules/open": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", @@ -32460,9 +32527,9 @@ } }, "node_modules/svgo": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", - "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz", + "integrity": "sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==", "dev": true, "license": "MIT", "dependencies": { @@ -33192,15 +33259,15 @@ } }, "node_modules/ts-checker-rspack-plugin": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ts-checker-rspack-plugin/-/ts-checker-rspack-plugin-1.5.1.tgz", - "integrity": "sha512-h0jF3PAIrG+UA2nZ2OPUEt0NywkpiJkydkoVg/Kd1OFPFlrayNBJoSP0zeNPUpdsFEt7ICX1BjENCfOH9nmMyA==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/ts-checker-rspack-plugin/-/ts-checker-rspack-plugin-1.5.2.tgz", + "integrity": "sha512-YtdNbLnJ9fU6itfQky0K9Lebb/N0TUSbSVUByyoGt27em3hiTCVnfW2MzxFUHpdMv5+7xPmoS8YBwdDyHfEaGQ==", "dev": true, "license": "MIT", "dependencies": { "@rspack/lite-tapable": "^1.1.2", "chokidar": "^3.6.0", - "memfs": "^4.57.7", + "memfs": "^4.57.8", "picocolors": "^1.1.1" }, "peerDependencies": { @@ -33252,14 +33319,14 @@ } }, "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-core": { - "version": "4.57.8", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.8.tgz", - "integrity": "sha512-YzVbwggV9452VCeHgo0bjsTaUt1O7JE0XpEsPar93nn/+RAwXk0mb1Y+f5EDJ3TRtRCFe+Ck5RuojdfB4jeHVw==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.64.0.tgz", + "integrity": "sha512-zs2TAq7Six5jgMuoMNjpspAvOP3mhtgq/k1UyQodEzCtQi/N83y2/y+zcvnZSGp/Rxq96DBN+bValOBQAyn/ew==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.8", - "@jsonjoy.com/fs-node-utils": "4.57.8", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", "thingies": "^2.5.0" }, "engines": { @@ -33274,15 +33341,15 @@ } }, "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.57.8", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.8.tgz", - "integrity": "sha512-vmClyvCQMxgqz7uamDiGtRfp4MjzOznk3pcQjCxlIwJcw7TWeyr+bF30hI0x8NxdtNOGMg1pHM74VDIXOeyjuw==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.64.0.tgz", + "integrity": "sha512-nMWOVbkLFyEgmXZih3wyvxA9XpgyyqyfrINMHvEFqhi7uqfRl7c9ERJt6yX7vgMPrB9Uo+OJO+Spa0cFzPD01w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.8", - "@jsonjoy.com/fs-node-builtins": "4.57.8", - "@jsonjoy.com/fs-node-utils": "4.57.8", + "@jsonjoy.com/fs-core": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", "thingies": "^2.5.0" }, "engines": { @@ -33297,17 +33364,17 @@ } }, "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-node": { - "version": "4.57.8", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.8.tgz", - "integrity": "sha512-IPEOlDYSnTDYpjQlQg2F8h+eqxKQN3sdbroI0WrteRiQZ462HzVpBo9ZZX485njz4nAacoe3fd4iDiIhk+k5Hg==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.64.0.tgz", + "integrity": "sha512-dO+NNkODbUli4uV42bcNrrLvq5rE7SNpdZ5TNd0dtbLsAaNK3MDiIC9lUi+brboGoIjW6vd2fB1qao60nrk5xA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.8", - "@jsonjoy.com/fs-node-builtins": "4.57.8", - "@jsonjoy.com/fs-node-utils": "4.57.8", - "@jsonjoy.com/fs-print": "4.57.8", - "@jsonjoy.com/fs-snapshot": "4.57.8", + "@jsonjoy.com/fs-core": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "@jsonjoy.com/fs-print": "4.64.0", + "@jsonjoy.com/fs-snapshot": "4.64.0", "glob-to-regex.js": "^1.0.0", "thingies": "^2.5.0" }, @@ -33323,9 +33390,9 @@ } }, "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.57.8", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.8.tgz", - "integrity": "sha512-mxXSXw8zZwRVakcjLqR2I/psy4gURFSASZS10kKJ2kJw05GC2nXGroGrWVHxwgkxXgQLsFQnB74QaLzsxzdL/w==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.64.0.tgz", + "integrity": "sha512-/o7WRFhUWaM/fOrslwLZGnzn4RmRILykn+lAL+mNObqqRNw+CQSiij6hpCeZ+C7buhdoVo7go/OYqzaSUfDYmA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -33340,15 +33407,15 @@ } }, "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.57.8", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.8.tgz", - "integrity": "sha512-AWZcT/4+H+iDl4XCukbXrarvwEgOrf/prFI5/7eg4ix9FxqVsZysIDJd1Kjd+AjlCeHKHJOaRqjLd5HiGSCJEw==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.64.0.tgz", + "integrity": "sha512-WDD9WVs0hb7UAEKTgZW2f66WDrbj7gIIWwpP3spbLyXa0rghtUaFTB8L4gdR3ZCWwiKIsj38/CNijpVmpnuPUw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-fsa": "4.57.8", - "@jsonjoy.com/fs-node-builtins": "4.57.8", - "@jsonjoy.com/fs-node-utils": "4.57.8" + "@jsonjoy.com/fs-fsa": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0" }, "engines": { "node": ">=10.0" @@ -33362,13 +33429,14 @@ } }, "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.57.8", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.8.tgz", - "integrity": "sha512-E/bJ7sQAb4pu9nbeJhbULU3WnqWrswte4N9Js/oHt7aHB746S8/XBqKlcbrqIgnD3095XluovNEZuu5ONT230g==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.64.0.tgz", + "integrity": "sha512-k5Indsx9hWW9xSF7Y6oSKKwtCUNhzZxadub3owhIlitc+iMRVlPPdX2duTKQWBL3qNWpXya8jykgaaWpheeS4w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.8" + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "glob-to-regex.js": "^1.0.1" }, "engines": { "node": ">=10.0" @@ -33382,13 +33450,13 @@ } }, "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-print": { - "version": "4.57.8", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.8.tgz", - "integrity": "sha512-DfzhOBpmvNu5P/KSe4NNQaOnvNliTdcf0qrh/4EReErF/XUQXYkd0vZl/OiJCm/qjEEo8DWRstliw2/JNS84dA==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.64.0.tgz", + "integrity": "sha512-PHZFccchvkhWrwPWHjmVAhbC3vSHCtyZvlZfJJ3ho2bnzl450hXri6/8e6pbkWdH+SkmLXNml0sV8e5HDAfxKw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.57.8", + "@jsonjoy.com/fs-node-utils": "4.64.0", "tree-dump": "^1.1.0" }, "engines": { @@ -33403,14 +33471,14 @@ } }, "node_modules/ts-checker-rspack-plugin/node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.57.8", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.8.tgz", - "integrity": "sha512-L+eqKaWOHLDaiMv1dh/EWQ4hA+o6xAhWSumTo3Teg7OM18jU/KE13/e8Mfal+eAZ/pSl4wIhKHcDiwapJzC8Wg==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.64.0.tgz", + "integrity": "sha512-oM7UDeL83q6NBzzsfKAsYKXKVXlykKFqqOLh4xZZKAzzROTlInkPbc6LTDGThEOnPiFiUzA7tYziHG9xavd76Q==", "dev": true, "license": "Apache-2.0", "dependencies": { "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.57.8", + "@jsonjoy.com/fs-node-utils": "4.64.0", "@jsonjoy.com/json-pack": "^17.65.0", "@jsonjoy.com/util": "^17.65.0" }, @@ -33560,20 +33628,20 @@ } }, "node_modules/ts-checker-rspack-plugin/node_modules/memfs": { - "version": "4.57.8", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.8.tgz", - "integrity": "sha512-bApYhn8BLpFAnAQmFfEl/NPN+8qx5Ar3V4Qt3ek23mVwBEElzV7c6XoPkb/PCG8ZFpowCEpHcPwMFTwHS7tSMA==", + "version": "4.64.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.64.0.tgz", + "integrity": "sha512-Kw72fgY7Wn+sD8KmtNWSafl1dz0UvAsE/PHs3YVfLiaZuA3HxNm9sRLqAu0ATiBGJvME1PxZXbBZPv5GycDeAw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.8", - "@jsonjoy.com/fs-fsa": "4.57.8", - "@jsonjoy.com/fs-node": "4.57.8", - "@jsonjoy.com/fs-node-builtins": "4.57.8", - "@jsonjoy.com/fs-node-to-fsa": "4.57.8", - "@jsonjoy.com/fs-node-utils": "4.57.8", - "@jsonjoy.com/fs-print": "4.57.8", - "@jsonjoy.com/fs-snapshot": "4.57.8", + "@jsonjoy.com/fs-core": "4.64.0", + "@jsonjoy.com/fs-fsa": "4.64.0", + "@jsonjoy.com/fs-node": "4.64.0", + "@jsonjoy.com/fs-node-builtins": "4.64.0", + "@jsonjoy.com/fs-node-to-fsa": "4.64.0", + "@jsonjoy.com/fs-node-utils": "4.64.0", + "@jsonjoy.com/fs-print": "4.64.0", + "@jsonjoy.com/fs-snapshot": "4.64.0", "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", diff --git a/package.json b/package.json index bc6b86836..f705874b6 100644 --- a/package.json +++ b/package.json @@ -161,16 +161,16 @@ "@eslint/js": "9.35.0", "@nestjs/schematics": "11.1.0", "@nestjs/testing": "11.1.27", - "@nx/angular": "23.0.1", - "@nx/eslint-plugin": "23.0.1", - "@nx/jest": "23.0.1", - "@nx/js": "23.0.1", - "@nx/module-federation": "23.0.1", - "@nx/nest": "23.0.1", - "@nx/node": "23.0.1", - "@nx/storybook": "23.0.1", - "@nx/web": "23.0.1", - "@nx/workspace": "23.0.1", + "@nx/angular": "23.0.2", + "@nx/eslint-plugin": "23.0.2", + "@nx/jest": "23.0.2", + "@nx/js": "23.0.2", + "@nx/module-federation": "23.0.2", + "@nx/nest": "23.0.2", + "@nx/node": "23.0.2", + "@nx/storybook": "23.0.2", + "@nx/web": "23.0.2", + "@nx/workspace": "23.0.2", "@prisma/config": "7.8.0", "@schematics/angular": "21.2.6", "@storybook/addon-docs": "10.1.10", @@ -198,7 +198,7 @@ "jest": "30.3.0", "jest-environment-jsdom": "30.2.0", "jest-preset-angular": "16.0.0", - "nx": "23.0.1", + "nx": "23.0.2", "prettier": "3.8.4", "prettier-plugin-organize-attributes": "1.0.0", "prisma": "7.8.0", From 00c37f026cc1c609d69579c9bc10ffcc9722c097 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:30:58 +0200 Subject: [PATCH 7/9] Task/harden validation of scraper configuration in asset profile endpoint (#7322) * Harden validation of scraper configuration * Update changelog --- CHANGELOG.md | 1 + libs/common/src/lib/dtos/index.ts | 2 ++ .../src/lib/dtos/scraper-configuration.dto.ts | 35 +++++++++++++++++++ .../src/lib/dtos/update-asset-profile.dto.ts | 3 ++ 4 files changed, 41 insertions(+) create mode 100644 libs/common/src/lib/dtos/scraper-configuration.dto.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 85df11fdb..a0a652c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Hardened the validation of the countries in the asset profile endpoints - Hardened the validation of the holdings in the asset profile endpoints +- Hardened the validation of the scraper configuration in the asset profile endpoint - Hardened the validation of the sectors in the asset profile endpoints - Rounded the value of the _Fear & Greed Index_ (market mood) in the twitter bot service - Set the change detection strategy to `OnPush` in the _X-ray_ page diff --git a/libs/common/src/lib/dtos/index.ts b/libs/common/src/lib/dtos/index.ts index c531313a6..785115356 100644 --- a/libs/common/src/lib/dtos/index.ts +++ b/libs/common/src/lib/dtos/index.ts @@ -12,6 +12,7 @@ import { CreateTagDto } from './create-tag.dto'; import { CreateWatchlistItemDto } from './create-watchlist-item.dto'; import { DeleteOwnUserDto } from './delete-own-user.dto'; import { HoldingDto } from './holding.dto'; +import { ScraperConfigurationDto } from './scraper-configuration.dto'; import { SectorDto } from './sector.dto'; import { TransferBalanceDto } from './transfer-balance.dto'; import { UpdateAccessDto } from './update-access.dto'; @@ -42,6 +43,7 @@ export { CreateWatchlistItemDto, DeleteOwnUserDto, HoldingDto, + ScraperConfigurationDto, SectorDto, TransferBalanceDto, UpdateAccessDto, diff --git a/libs/common/src/lib/dtos/scraper-configuration.dto.ts b/libs/common/src/lib/dtos/scraper-configuration.dto.ts new file mode 100644 index 000000000..52cadb80c --- /dev/null +++ b/libs/common/src/lib/dtos/scraper-configuration.dto.ts @@ -0,0 +1,35 @@ +import { + IsIn, + IsNumber, + IsObject, + IsOptional, + IsString, + IsUrl +} from 'class-validator'; + +export class ScraperConfigurationDto { + @IsNumber() + @IsOptional() + defaultMarketPrice?: number; + + @IsObject() + @IsOptional() + headers?: { [key: string]: string }; + + @IsOptional() + @IsString() + locale?: string; + + @IsIn(['instant', 'lazy']) + @IsOptional() + mode?: 'instant' | 'lazy'; + + @IsString() + selector: string; + + @IsUrl({ + protocols: ['http', 'https'], + require_protocol: true + }) + url: string; +} diff --git a/libs/common/src/lib/dtos/update-asset-profile.dto.ts b/libs/common/src/lib/dtos/update-asset-profile.dto.ts index 2204fd0ab..a6e2230e8 100644 --- a/libs/common/src/lib/dtos/update-asset-profile.dto.ts +++ b/libs/common/src/lib/dtos/update-asset-profile.dto.ts @@ -21,6 +21,7 @@ import { import { CountryDto } from './country.dto'; import { HoldingDto } from './holding.dto'; +import { ScraperConfigurationDto } from './scraper-configuration.dto'; import { SectorDto } from './sector.dto'; export class UpdateAssetProfileDto { @@ -70,6 +71,8 @@ export class UpdateAssetProfileDto { @IsObject() @IsOptional() + @Type(() => ScraperConfigurationDto) + @ValidateNested() scraperConfiguration?: Prisma.InputJsonObject; @IsArray() From 69254032a384655d2f8447674bc53a9508ed068e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:53:48 +0200 Subject: [PATCH 8/9] Feature/add markets endpoint for Fear & Greed index to Ghostfolio data provider (#7323) * Add markets endpoint * Update changelog --- CHANGELOG.md | 4 ++ .../ghostfolio/ghostfolio.controller.ts | 39 ++++++++++++++ .../ghostfolio/ghostfolio.module.ts | 2 + .../ghostfolio/ghostfolio.service.ts | 33 +++++++++++- .../market-data/market-data.controller.ts | 43 ++------------- .../market-data/market-data.module.ts | 8 +-- apps/api/src/app/symbol/symbol.service.ts | 46 ++++++++++++++++ .../src/app/pages/api/api-page.component.ts | 22 +++++++- apps/client/src/app/pages/api/api-page.html | 52 +++++++++++++++++++ 9 files changed, 200 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0a652c44..4586c668d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the markets endpoint for the _Fear & Greed Index_ (market mood) to the `GHOSTFOLIO` data provider + ### Changed - Hardened the validation of the countries in the asset profile endpoints diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts index 04165e9a1..5d6979acc 100644 --- a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts @@ -8,6 +8,7 @@ import { DividendsResponse, HistoricalResponse, LookupResponse, + MarketDataOfMarketsResponse, QuotesResponse } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; @@ -19,6 +20,7 @@ import { HttpException, Inject, Param, + ParseIntPipe, Query, UseGuards, Version @@ -203,6 +205,43 @@ export class GhostfolioController { } } + @Get('markets') + @HasPermission(permissions.enableDataProviderGhostfolio) + @UseGuards(AuthGuard('api-key'), HasPermissionGuard) + public async getMarketDataOfMarkets( + @Query('includeHistoricalData', new ParseIntPipe({ optional: true })) + includeHistoricalData = 0 + ): Promise { + const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); + + if ( + this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), + StatusCodes.TOO_MANY_REQUESTS + ); + } + + try { + const marketDataOfMarkets = + await this.ghostfolioService.getMarketDataOfMarkets({ + includeHistoricalData + }); + + await this.ghostfolioService.incrementDailyRequests({ + userId: this.request.user.id + }); + + return marketDataOfMarkets; + } catch { + throw new HttpException( + getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), + StatusCodes.INTERNAL_SERVER_ERROR + ); + } + } + @Get('quotes') @HasPermission(permissions.enableDataProviderGhostfolio) @UseGuards(AuthGuard('api-key'), HasPermissionGuard) diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts index 484f30ee3..1b8788ecb 100644 --- a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts @@ -1,4 +1,5 @@ import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; +import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module'; import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alpha-vantage/alpha-vantage.service'; @@ -33,6 +34,7 @@ import { GhostfolioService } from './ghostfolio.service'; PrismaModule, PropertyModule, RedisCacheModule, + SymbolModule, SymbolProfileModule ], providers: [ diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts index caff86ae0..abbb5cbf6 100644 --- a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts +++ b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts @@ -1,3 +1,4 @@ +import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { GhostfolioService as GhostfolioDataProviderService } from '@ghostfolio/api/services/data-provider/ghostfolio/ghostfolio.service'; @@ -25,6 +26,7 @@ import { HistoricalResponse, LookupItem, LookupResponse, + MarketDataOfMarketsResponse, QuotesResponse } from '@ghostfolio/common/interfaces'; import { UserWithSettings } from '@ghostfolio/common/types'; @@ -32,6 +34,7 @@ import { UserWithSettings } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; import { DataSource, SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; +import { isEmpty } from 'lodash'; @Injectable() export class GhostfolioService { @@ -42,7 +45,8 @@ export class GhostfolioService { private readonly dataProviderService: DataProviderService, private readonly fetchService: FetchService, private readonly prismaService: PrismaService, - private readonly propertyService: PropertyService + private readonly propertyService: PropertyService, + private readonly symbolService: SymbolService ) {} public async getAssetProfile({ symbol }: GetAssetProfileParams) { @@ -198,6 +202,33 @@ export class GhostfolioService { } } + public async getMarketDataOfMarkets({ + includeHistoricalData + }: { + includeHistoricalData: number; + }): Promise { + try { + const marketDataOfMarkets = + await this.symbolService.getMarketDataOfMarkets({ + includeHistoricalData + }); + + for (const symbolItem of Object.values( + marketDataOfMarkets.fearAndGreedIndex + )) { + if (!isEmpty(symbolItem)) { + symbolItem.dataSource = DataSource.GHOSTFOLIO; + } + } + + return marketDataOfMarkets; + } catch (error) { + this.logger.error(error); + + throw error; + } + } + public async getMaxDailyRequests() { return parseInt( (await this.propertyService.getByKey( diff --git a/apps/api/src/app/endpoints/market-data/market-data.controller.ts b/apps/api/src/app/endpoints/market-data/market-data.controller.ts index 105bb0780..03d50c284 100644 --- a/apps/api/src/app/endpoints/market-data/market-data.controller.ts +++ b/apps/api/src/app/endpoints/market-data/market-data.controller.ts @@ -1,14 +1,8 @@ import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; -import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; -import { - ghostfolioFearAndGreedIndexDataSourceCryptocurrencies, - ghostfolioFearAndGreedIndexSymbolCryptocurrencies, - ghostfolioFearAndGreedIndexSymbolStocks -} from '@ghostfolio/common/config'; import { UpdateBulkMarketDataDto } from '@ghostfolio/common/dtos'; import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper'; import { MarketDataOfMarketsResponse } from '@ghostfolio/common/interfaces'; @@ -36,7 +30,6 @@ import { getReasonPhrase, StatusCodes } from 'http-status-codes'; @Controller('market-data') export class MarketDataController { public constructor( - private readonly dataProviderService: DataProviderService, private readonly marketDataService: MarketDataService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly symbolProfileService: SymbolProfileService, @@ -50,39 +43,9 @@ export class MarketDataController { @Query('includeHistoricalData', new ParseIntPipe({ optional: true })) includeHistoricalData = 0 ): Promise { - const [ - marketDataFearAndGreedIndexCryptocurrencies, - marketDataFearAndGreedIndexStocks - ] = await Promise.all([ - this.symbolService.get({ - includeHistoricalData, - dataGatheringItem: { - dataSource: ghostfolioFearAndGreedIndexDataSourceCryptocurrencies, - symbol: ghostfolioFearAndGreedIndexSymbolCryptocurrencies - }, - useIntradayData: true - }), - this.symbolService.get({ - includeHistoricalData, - dataGatheringItem: { - dataSource: - this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(), - symbol: ghostfolioFearAndGreedIndexSymbolStocks - }, - useIntradayData: true - }) - ]); - - return { - fearAndGreedIndex: { - CRYPTOCURRENCIES: { - ...marketDataFearAndGreedIndexCryptocurrencies - }, - STOCKS: { - ...marketDataFearAndGreedIndexStocks - } - } - }; + return this.symbolService.getMarketDataOfMarkets({ + includeHistoricalData + }); } @Post(':dataSource/:symbol') diff --git a/apps/api/src/app/endpoints/market-data/market-data.module.ts b/apps/api/src/app/endpoints/market-data/market-data.module.ts index 47ce11502..1de10907b 100644 --- a/apps/api/src/app/endpoints/market-data/market-data.module.ts +++ b/apps/api/src/app/endpoints/market-data/market-data.module.ts @@ -1,5 +1,4 @@ import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; -import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; @@ -9,11 +8,6 @@ import { MarketDataController } from './market-data.controller'; @Module({ controllers: [MarketDataController], - imports: [ - DataProviderModule, - MarketDataServiceModule, - SymbolModule, - SymbolProfileModule - ] + imports: [MarketDataServiceModule, SymbolModule, SymbolProfileModule] }) export class MarketDataModule {} diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index 39d279de1..2ace4feeb 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -1,6 +1,11 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; +import { + ghostfolioFearAndGreedIndexDataSourceCryptocurrencies, + ghostfolioFearAndGreedIndexSymbolCryptocurrencies, + ghostfolioFearAndGreedIndexSymbolStocks +} from '@ghostfolio/common/config'; import { DATE_FORMAT, getAssetProfileIdentifier @@ -9,6 +14,7 @@ import { DataProviderHistoricalResponse, HistoricalDataItem, LookupResponse, + MarketDataOfMarketsResponse, SymbolItem } from '@ghostfolio/common/interfaces'; import { UserWithSettings } from '@ghostfolio/common/types'; @@ -122,6 +128,46 @@ export class SymbolService { }; } + public async getMarketDataOfMarkets({ + includeHistoricalData + }: { + includeHistoricalData: number; + }): Promise { + const [ + marketDataFearAndGreedIndexCryptocurrencies, + marketDataFearAndGreedIndexStocks + ] = await Promise.all([ + this.get({ + includeHistoricalData, + dataGatheringItem: { + dataSource: ghostfolioFearAndGreedIndexDataSourceCryptocurrencies, + symbol: ghostfolioFearAndGreedIndexSymbolCryptocurrencies + }, + useIntradayData: true + }), + this.get({ + includeHistoricalData, + dataGatheringItem: { + dataSource: + this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(), + symbol: ghostfolioFearAndGreedIndexSymbolStocks + }, + useIntradayData: true + }) + ]); + + return { + fearAndGreedIndex: { + CRYPTOCURRENCIES: { + ...marketDataFearAndGreedIndexCryptocurrencies + }, + STOCKS: { + ...marketDataFearAndGreedIndexStocks + } + } + }; + } + public async lookup({ includeIndices = false, query, diff --git a/apps/client/src/app/pages/api/api-page.component.ts b/apps/client/src/app/pages/api/api-page.component.ts index 0f39c67d6..3373977cc 100644 --- a/apps/client/src/app/pages/api/api-page.component.ts +++ b/apps/client/src/app/pages/api/api-page.component.ts @@ -1,3 +1,4 @@ +import { GfFearAndGreedIndexComponent } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.component'; import { HEADER_KEY_SKIP_INTERCEPTOR, HEADER_KEY_TOKEN @@ -10,6 +11,7 @@ import { DividendsResponse, HistoricalResponse, LookupResponse, + MarketDataOfMarketsResponse, QuotesResponse } from '@ghostfolio/common/interfaces'; @@ -32,7 +34,12 @@ import { FetchFailure, FetchResult } from './interfaces/interfaces'; @Component({ host: { class: 'page' }, - imports: [CommonModule, MatCardModule, NgxSkeletonLoaderModule], + imports: [ + CommonModule, + GfFearAndGreedIndexComponent, + MatCardModule, + NgxSkeletonLoaderModule + ], selector: 'gf-api-page', styleUrls: ['./api-page.scss'], templateUrl: './api-page.html' @@ -48,6 +55,9 @@ export class GfApiPageComponent implements OnInit { >; protected isinLookupItems$: Observable>; protected lookupItems$: Observable>; + protected marketDataOfMarkets$: Observable< + FetchResult + >; protected quotes$: Observable>; protected status$: Observable< FetchResult @@ -68,6 +78,7 @@ export class GfApiPageComponent implements OnInit { this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL' }); this.isinLookupItems$ = this.fetchLookupItems({ query: 'US0378331005' }); this.lookupItems$ = this.fetchLookupItems({ query: 'apple' }); + this.marketDataOfMarkets$ = this.fetchMarketDataOfMarkets(); this.quotes$ = this.fetchQuotes({ symbols: ['AAPL', 'VOO'] }); this.status$ = this.fetchStatus(); } @@ -172,6 +183,15 @@ export class GfApiPageComponent implements OnInit { ); } + private fetchMarketDataOfMarkets() { + return this.http + .get( + '/api/v1/data-providers/ghostfolio/markets', + { headers: this.getHeaders() } + ) + .pipe(this.catchFetchFailure(), takeUntilDestroyed(this.destroyRef)); + } + private fetchQuotes({ symbols }: { symbols: string[] }) { const params = new HttpParams().set('symbols', symbols.join(',')); diff --git a/apps/client/src/app/pages/api/api-page.html b/apps/client/src/app/pages/api/api-page.html index 07f5ec981..b2d9d03ac 100644 --- a/apps/client/src/app/pages/api/api-page.html +++ b/apps/client/src/app/pages/api/api-page.html @@ -168,6 +168,58 @@ } +
+ @let marketDataOfMarkets = marketDataOfMarkets$ | async; +
+ + + Markets + Stocks + + + @if (isFetchFailure(marketDataOfMarkets)) { + 🔴 {{ marketDataOfMarkets.fetchError }} + } @else if (marketDataOfMarkets) { + + } @else { + + } + + +
+
+ + + Markets + Cryptocurrencies + + + @if (isFetchFailure(marketDataOfMarkets)) { + 🔴 {{ marketDataOfMarkets.fetchError }} + } @else if (marketDataOfMarkets) { + + } @else { + + } + + +
+
From 5d40f2fb1e9311a92386139a2651d307b94ce34c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:54:17 +0200 Subject: [PATCH 9/9] Task/upgrade countries-list to version 3.4.0 (#7314) * Update countries-list to version 3.4.0 * Update changelog --- CHANGELOG.md | 1 + package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4586c668d..c012d540c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rounded the value of the _Fear & Greed Index_ (market mood) in the twitter bot service - Set the change detection strategy to `OnPush` in the _X-ray_ page - Deprecated `SymbolProfile` in favor of `assetProfile` in the activity interface +- Upgraded `countries-list` from version `3.3.0` to `3.4.0` - Upgraded `Nx` from version `23.0.1` to `23.0.2` ## 3.25.0 - 2026-07-12 diff --git a/package-lock.json b/package-lock.json index ca2585f35..9cc8f9160 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,7 @@ "color": "5.0.3", "cookie-parser": "1.4.7", "countries-and-timezones": "3.9.0", - "countries-list": "3.3.0", + "countries-list": "3.4.0", "countup.js": "2.10.0", "date-fns": "4.4.0", "dotenv": "17.2.3", @@ -17062,9 +17062,9 @@ } }, "node_modules/countries-list": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.3.0.tgz", - "integrity": "sha512-XRUjS+dcZuNh/fg3+mka3bXgcg4TbQZ1gaK5IJqO6qulerBANl1bmrd20P2dgmPkBpP+5FnejiSF1gd7bgAg+g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/countries-list/-/countries-list-3.4.0.tgz", + "integrity": "sha512-SonKkjlEGUNz/Ugj6akOMUl8/F6EBVbLq/nQbDKhZo60EFyNgsVIgDP521J9pGhywikV514Ct4BueIqiOnvBBA==", "license": "MIT" }, "node_modules/countup.js": { diff --git a/package.json b/package.json index f705874b6..c484e3617 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "color": "5.0.3", "cookie-parser": "1.4.7", "countries-and-timezones": "3.9.0", - "countries-list": "3.3.0", + "countries-list": "3.4.0", "countup.js": "2.10.0", "date-fns": "4.4.0", "dotenv": "17.2.3",