From e314efb2e1dfb6581f40cc6e8951da449269c9c9 Mon Sep 17 00:00:00 2001 From: Vinodh Zamboulingame <48854069+vzamboulingame@users.noreply.github.com> Date: Fri, 2 May 2025 08:26:04 +0200 Subject: [PATCH 1/9] Feature/improve language localization for FR 20250501 (#4637) * Improve french translation * Update changelog --- CHANGELOG.md | 6 ++++++ apps/client/src/locales/messages.fr.xlf | 12 ++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4359aa77..d40df6fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Improved the language localization for Français (`fr`) + ## 2.158.0 - 2025-04-30 ### Added diff --git a/apps/client/src/locales/messages.fr.xlf b/apps/client/src/locales/messages.fr.xlf index a9b3cd04..e554ce9f 100644 --- a/apps/client/src/locales/messages.fr.xlf +++ b/apps/client/src/locales/messages.fr.xlf @@ -2831,7 +2831,7 @@ Hello, has shared a Portfolio with you! - Bonjour, a partagé un Portefeuille avec vous ! + Bonjour, a partagé un Portefeuille avec vous ! apps/client/src/app/pages/public/public-page.html 4 @@ -7931,7 +7931,7 @@ Add asset to watchlist - Add asset to watchlist + Ajouter un actif à la liste de suivi apps/client/src/app/components/home-watchlist/create-watchlist-item-dialog/create-watchlist-item-dialog.html 7 @@ -7939,7 +7939,7 @@ Watchlist - Watchlist + Liste de suivi apps/client/src/app/components/home-watchlist/home-watchlist.html 4 @@ -7947,7 +7947,7 @@ Watchlist - Watchlist + Liste de suivi apps/client/src/app/pages/home/home-page-routing.module.ts 44 @@ -7959,7 +7959,7 @@ Get Early Access - Get Early Access + Obtenir un accès anticipé apps/client/src/app/components/admin-settings/ghostfolio-premium-api-dialog/ghostfolio-premium-api-dialog.html 29 @@ -7967,7 +7967,7 @@ Do you really want to delete this item? - Do you really want to delete this item? + Voulez-vous vraiment supprimer cet élément? libs/ui/src/lib/benchmark/benchmark.component.ts 122 From 6bb85c4fb80ff0bf6d300cf6d662baba8ef952d5 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 2 May 2025 16:09:27 +0200 Subject: [PATCH 2/9] Bugfix/allow GBp in currency code validation (#4640) * Allow GBp in currency code validation * Update changelog --- CHANGELOG.md | 4 ++++ apps/api/src/validators/is-currency-code.ts | 16 +++++----------- libs/common/src/lib/helper.ts | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d40df6fb..984e7b08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the language localization for Français (`fr`) +### Fixed + +- Fixed the currency code validation by allowing `GBp` + ## 2.158.0 - 2025-04-30 ### Added diff --git a/apps/api/src/validators/is-currency-code.ts b/apps/api/src/validators/is-currency-code.ts index d04da780..771818b0 100644 --- a/apps/api/src/validators/is-currency-code.ts +++ b/apps/api/src/validators/is-currency-code.ts @@ -1,4 +1,4 @@ -import { DERIVED_CURRENCIES } from '@ghostfolio/common/config'; +import { isDerivedCurrency } from '@ghostfolio/common/helper'; import { registerDecorator, @@ -28,17 +28,11 @@ export class IsExtendedCurrencyConstraint return '$property must be a valid ISO4217 currency code'; } - public validate(currency: any) { - // Return true if currency is a standard ISO 4217 code or a derived currency + public validate(currency: string) { + // Return true if currency is a derived currency or a standard ISO 4217 code return ( - this.isUpperCase(currency) && - (isISO4217CurrencyCode(currency) || - [ - ...DERIVED_CURRENCIES.map((derivedCurrency) => { - return derivedCurrency.currency; - }), - 'USX' - ].includes(currency)) + isDerivedCurrency(currency) || + (this.isUpperCase(currency) && isISO4217CurrencyCode(currency)) ); } diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index e5104a99..e5dc187f 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -349,7 +349,7 @@ export function isDerivedCurrency(aCurrency: string) { return true; } - return DERIVED_CURRENCIES.find(({ currency }) => { + return DERIVED_CURRENCIES.some(({ currency }) => { return currency === aCurrency; }); } From 770b322137edff39c6e4fc2f9c3ecbce95cdaf66 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Fri, 2 May 2025 21:11:24 +0700 Subject: [PATCH 3/9] Feature/extend watchlist endpoint by name, performances and market condition (#4634) * Extend watchlist endpoint by name, performances and market condition * Update changelog --- CHANGELOG.md | 4 ++ .../endpoints/watchlist/watchlist.module.ts | 4 ++ .../endpoints/watchlist/watchlist.service.ts | 54 +++++++++++++++++-- .../services/benchmark/benchmark.service.ts | 24 ++++----- .../home-watchlist.component.ts | 21 ++++---- .../home-watchlist/home-watchlist.html | 2 +- .../responses/watchlist-response.interface.ts | 11 +++- 7 files changed, 93 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 984e7b08..85df415b 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 + +- Extended the watchlist by the date of the last all time high, the current change to the all time high and the current market condition (experimental) + ### Changed - Improved the language localization for Français (`fr`) diff --git a/apps/api/src/app/endpoints/watchlist/watchlist.module.ts b/apps/api/src/app/endpoints/watchlist/watchlist.module.ts index 2addd2de..a2d32d1d 100644 --- a/apps/api/src/app/endpoints/watchlist/watchlist.module.ts +++ b/apps/api/src/app/endpoints/watchlist/watchlist.module.ts @@ -1,6 +1,8 @@ import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; +import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; +import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; @@ -13,8 +15,10 @@ import { WatchlistService } from './watchlist.service'; @Module({ controllers: [WatchlistController], imports: [ + BenchmarkModule, DataGatheringModule, DataProviderModule, + MarketDataModule, PrismaModule, SymbolProfileModule, TransformDataSourceInRequestModule, diff --git a/apps/api/src/app/endpoints/watchlist/watchlist.service.ts b/apps/api/src/app/endpoints/watchlist/watchlist.service.ts index 6ff71ec5..36a498e1 100644 --- a/apps/api/src/app/endpoints/watchlist/watchlist.service.ts +++ b/apps/api/src/app/endpoints/watchlist/watchlist.service.ts @@ -1,8 +1,10 @@ +import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; +import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; -import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; +import { WatchlistResponse } from '@ghostfolio/common/interfaces'; import { BadRequestException, Injectable } from '@nestjs/common'; import { DataSource, Prisma } from '@prisma/client'; @@ -10,8 +12,10 @@ import { DataSource, Prisma } from '@prisma/client'; @Injectable() export class WatchlistService { public constructor( + private readonly benchmarkService: BenchmarkService, private readonly dataGatheringService: DataGatheringService, private readonly dataProviderService: DataProviderService, + private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, private readonly symbolProfileService: SymbolProfileService ) {} @@ -87,7 +91,7 @@ export class WatchlistService { public async getWatchlistItems( userId: string - ): Promise { + ): Promise { const user = await this.prismaService.user.findUnique({ select: { watchlist: { @@ -97,6 +101,50 @@ export class WatchlistService { where: { id: userId } }); - return user.watchlist ?? []; + const [assetProfiles, quotes] = await Promise.all([ + this.symbolProfileService.getSymbolProfiles(user.watchlist), + this.dataProviderService.getQuotes({ + items: user.watchlist.map(({ dataSource, symbol }) => { + return { dataSource, symbol }; + }) + }) + ]); + + const watchlist = await Promise.all( + user.watchlist.map(async ({ dataSource, symbol }) => { + const assetProfile = assetProfiles.find((profile) => { + return profile.dataSource === dataSource && profile.symbol === symbol; + }); + + const allTimeHigh = await this.marketDataService.getMax({ + dataSource, + symbol + }); + + const performancePercent = + this.benchmarkService.calculateChangeInPercentage( + allTimeHigh?.marketPrice, + quotes[symbol]?.marketPrice + ); + + return { + dataSource, + symbol, + marketCondition: + this.benchmarkService.getMarketCondition(performancePercent), + name: assetProfile?.name, + performances: { + allTimeHigh: { + performancePercent, + date: allTimeHigh?.date + } + } + }; + }) + ); + + return watchlist.sort((a, b) => { + return a.name.localeCompare(b.name); + }); } } diff --git a/apps/api/src/services/benchmark/benchmark.service.ts b/apps/api/src/services/benchmark/benchmark.service.ts index 95cb9e5d..f37f26bf 100644 --- a/apps/api/src/services/benchmark/benchmark.service.ts +++ b/apps/api/src/services/benchmark/benchmark.service.ts @@ -212,6 +212,18 @@ export class BenchmarkService { }; } + public getMarketCondition( + aPerformanceInPercent: number + ): Benchmark['marketCondition'] { + if (aPerformanceInPercent >= 0) { + return 'ALL_TIME_HIGH'; + } else if (aPerformanceInPercent <= -0.2) { + return 'BEAR_MARKET'; + } else { + return 'NEUTRAL_MARKET'; + } + } + private async calculateAndCacheBenchmarks({ enableSharing = false }): Promise { @@ -302,16 +314,4 @@ export class BenchmarkService { return benchmarks; } - - private getMarketCondition( - aPerformanceInPercent: number - ): Benchmark['marketCondition'] { - if (aPerformanceInPercent >= 0) { - return 'ALL_TIME_HIGH'; - } else if (aPerformanceInPercent <= -0.2) { - return 'BEAR_MARKET'; - } else { - return 'NEUTRAL_MARKET'; - } - } } diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts index daf512b3..efad2fef 100644 --- a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts +++ b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts @@ -6,6 +6,7 @@ import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { BenchmarkTrend } from '@ghostfolio/common/types'; import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; @@ -118,15 +119,17 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit { .fetchWatchlist() .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ watchlist }) => { - this.watchlist = watchlist.map(({ dataSource, symbol }) => ({ - dataSource, - symbol, - marketCondition: null, - name: symbol, - performances: null, - trend50d: 'UNKNOWN', - trend200d: 'UNKNOWN' - })); + this.watchlist = watchlist.map( + ({ dataSource, marketCondition, name, performances, symbol }) => ({ + dataSource, + marketCondition, + name, + performances, + symbol, + trend50d: 'UNKNOWN' as BenchmarkTrend, + trend200d: 'UNKNOWN' as BenchmarkTrend + }) + ); this.changeDetectorRef.markForCheck(); }); diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.html b/apps/client/src/app/components/home-watchlist/home-watchlist.html index ef073d33..d290a4a2 100644 --- a/apps/client/src/app/components/home-watchlist/home-watchlist.html +++ b/apps/client/src/app/components/home-watchlist/home-watchlist.html @@ -7,7 +7,7 @@ } -
+
Date: Fri, 2 May 2025 16:46:43 +0200 Subject: [PATCH 4/9] Feature/improve watchlist for impersonation mode (#4632) * Improve watchlist for impersonation mode * Update changelog --- CHANGELOG.md | 1 + .../watchlist/watchlist.controller.ts | 13 ++++++-- .../endpoints/watchlist/watchlist.module.ts | 2 ++ .../home-watchlist.component.ts | 30 ++++++++++++++----- .../home-watchlist/home-watchlist.html | 2 +- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85df415b..7bbdaf43 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 ### Added - Extended the watchlist by the date of the last all time high, the current change to the all time high and the current market condition (experimental) +- Added support for the impersonation mode in the watchlist (experimental) ### Changed diff --git a/apps/api/src/app/endpoints/watchlist/watchlist.controller.ts b/apps/api/src/app/endpoints/watchlist/watchlist.controller.ts index c9e41d5d..8d9d322a 100644 --- a/apps/api/src/app/endpoints/watchlist/watchlist.controller.ts +++ b/apps/api/src/app/endpoints/watchlist/watchlist.controller.ts @@ -2,6 +2,8 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; +import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; +import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { WatchlistResponse } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; import { RequestWithUser } from '@ghostfolio/common/types'; @@ -11,6 +13,7 @@ import { Controller, Delete, Get, + Headers, HttpException, Inject, Param, @@ -29,6 +32,7 @@ import { WatchlistService } from './watchlist.service'; @Controller('watchlist') export class WatchlistController { public constructor( + private readonly impersonationService: ImpersonationService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly watchlistService: WatchlistService ) {} @@ -79,9 +83,14 @@ export class WatchlistController { @HasPermission(permissions.readWatchlist) @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(TransformDataSourceInResponseInterceptor) - public async getWatchlistItems(): Promise { + public async getWatchlistItems( + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string + ): Promise { + const impersonationUserId = + await this.impersonationService.validateImpersonationId(impersonationId); + const watchlist = await this.watchlistService.getWatchlistItems( - this.request.user.id + impersonationUserId || this.request.user.id ); return { diff --git a/apps/api/src/app/endpoints/watchlist/watchlist.module.ts b/apps/api/src/app/endpoints/watchlist/watchlist.module.ts index a2d32d1d..ce9ae12b 100644 --- a/apps/api/src/app/endpoints/watchlist/watchlist.module.ts +++ b/apps/api/src/app/endpoints/watchlist/watchlist.module.ts @@ -2,6 +2,7 @@ import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; +import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { DataGatheringModule } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.module'; @@ -18,6 +19,7 @@ import { WatchlistService } from './watchlist.service'; BenchmarkModule, DataGatheringModule, DataProviderModule, + ImpersonationModule, MarketDataModule, PrismaModule, SymbolProfileModule, diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts index efad2fef..5c0b3fa5 100644 --- a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts +++ b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts @@ -1,4 +1,5 @@ import { DataService } from '@ghostfolio/client/services/data.service'; +import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { AssetProfileIdentifier, @@ -45,6 +46,7 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/ }) export class HomeWatchlistComponent implements OnDestroy, OnInit { public deviceType: string; + public hasImpersonationId: boolean; public hasPermissionToCreateWatchlistItem: boolean; public hasPermissionToDeleteWatchlistItem: boolean; public user: User; @@ -57,12 +59,20 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, + private impersonationStorageService: ImpersonationStorageService, private route: ActivatedRoute, private router: Router, private userService: UserService ) { this.deviceType = this.deviceService.getDeviceInfo().deviceType; + this.impersonationStorageService + .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((impersonationId) => { + this.hasImpersonationId = !!impersonationId; + }); + this.route.queryParams .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((params) => { @@ -77,14 +87,18 @@ export class HomeWatchlistComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; - this.hasPermissionToCreateWatchlistItem = hasPermission( - this.user.permissions, - permissions.createWatchlistItem - ); - this.hasPermissionToDeleteWatchlistItem = hasPermission( - this.user.permissions, - permissions.deleteWatchlistItem - ); + this.hasPermissionToCreateWatchlistItem = + !this.hasImpersonationId && + hasPermission( + this.user.permissions, + permissions.createWatchlistItem + ); + this.hasPermissionToDeleteWatchlistItem = + !this.hasImpersonationId && + hasPermission( + this.user.permissions, + permissions.deleteWatchlistItem + ); this.changeDetectorRef.markForCheck(); } diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.html b/apps/client/src/app/components/home-watchlist/home-watchlist.html index d290a4a2..9149eab9 100644 --- a/apps/client/src/app/components/home-watchlist/home-watchlist.html +++ b/apps/client/src/app/components/home-watchlist/home-watchlist.html @@ -20,7 +20,7 @@
-@if (hasPermissionToCreateWatchlistItem) { +@if (!hasImpersonationId && hasPermissionToCreateWatchlistItem) {
Date: Fri, 2 May 2025 16:47:18 +0200 Subject: [PATCH 5/9] Feature/upgrade bootstrap to version 4.6.2 (#4631) * Upgrade bootstrap to version 4.6.2 * Update changelog --- CHANGELOG.md | 1 + package-lock.json | 22 ++++++++++++++-------- package.json | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bbdaf43..9b562a3d 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 ### Changed - Improved the language localization for Français (`fr`) +- Upgraded `bootstrap` from version `4.6.0` to `4.6.2` ### Fixed diff --git a/package-lock.json b/package-lock.json index 27acd282..3472e889 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "@stripe/stripe-js": "5.4.0", "alphavantage": "2.2.0", "big.js": "6.2.2", - "bootstrap": "4.6.0", + "bootstrap": "4.6.2", "bull": "4.16.5", "cache-manager": "5.7.6", "cache-manager-redis-yet": "5.1.4", @@ -14322,14 +14322,20 @@ "license": "ISC" }, "node_modules/bootstrap": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz", - "integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", + "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - }, "peerDependencies": { "jquery": "1.9.1 - 3", "popper.js": "^1.16.1" diff --git a/package.json b/package.json index c6416388..29c2a69e 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "@stripe/stripe-js": "5.4.0", "alphavantage": "2.2.0", "big.js": "6.2.2", - "bootstrap": "4.6.0", + "bootstrap": "4.6.2", "bull": "4.16.5", "cache-manager": "5.7.6", "cache-manager-redis-yet": "5.1.4", From c3887e2f8eff06870fb15a5288bbcde9637635ee Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 2 May 2025 17:01:39 +0200 Subject: [PATCH 6/9] Release 2.159.0 (#4643) --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b562a3d..f9f4a443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## 2.159.0 - 2025-05-02 ### Added diff --git a/package-lock.json b/package-lock.json index 3472e889..6e4e2c30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "2.158.0", + "version": "2.159.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "2.158.0", + "version": "2.159.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 29c2a69e..f591c933 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.158.0", + "version": "2.159.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", From 3e963228d6f99ec5c4408ca5056b3f0e5844a9ae Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 2 May 2025 19:15:11 +0200 Subject: [PATCH 7/9] Feature/refactor accounts response interface (#4644) * Refactor accounts response interface --- apps/api/src/app/account/account.controller.ts | 4 ++-- apps/api/src/app/portfolio/portfolio.service.ts | 4 ++-- apps/client/src/app/services/data.service.ts | 4 ++-- libs/common/src/lib/interfaces/index.ts | 4 ++-- .../accounts-response.interface.ts} | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) rename libs/common/src/lib/interfaces/{accounts.interface.ts => responses/accounts-response.interface.ts} (84%) diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 71ff8a04..8512d840 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -9,7 +9,7 @@ import { ImpersonationService } from '@ghostfolio/api/services/impersonation/imp import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { AccountBalancesResponse, - Accounts + AccountsResponse } from '@ghostfolio/common/interfaces'; import { permissions } from '@ghostfolio/common/permissions'; import type { @@ -90,7 +90,7 @@ export class AccountController { @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, @Query('dataSource') filterByDataSource?: string, @Query('symbol') filterBySymbol?: string - ): Promise { + ): Promise { const impersonationUserId = await this.impersonationService.validateImpersonationId(impersonationId); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 3aa9696b..a2480974 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -35,7 +35,7 @@ import { } from '@ghostfolio/common/config'; import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper'; import { - Accounts, + AccountsResponse, EnhancedSymbolProfile, Filter, HistoricalDataItem, @@ -209,7 +209,7 @@ export class PortfolioService { filters?: Filter[]; userId: string; withExcludedAccounts?: boolean; - }): Promise { + }): Promise { const accounts = await this.getAccounts({ filters, userId, diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 353bc415..830543dd 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -25,7 +25,7 @@ import { Access, AccessTokenResponse, AccountBalancesResponse, - Accounts, + AccountsResponse, AiPromptResponse, ApiKeyResponse, AssetProfileIdentifier, @@ -191,7 +191,7 @@ export class DataService { public fetchAccounts({ filters }: { filters?: Filter[] } = {}) { const params = this.buildFiltersAsQueryParams({ filters }); - return this.http.get('/api/v1/account', { params }); + return this.http.get('/api/v1/account', { params }); } public fetchActivities({ diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index b83b6d5f..3e0528fd 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -1,6 +1,5 @@ import type { Access } from './access.interface'; import type { AccountBalance } from './account-balance.interface'; -import type { Accounts } from './accounts.interface'; import type { AdminData } from './admin-data.interface'; import type { AdminJobs } from './admin-jobs.interface'; import type { AdminMarketDataDetails } from './admin-market-data-details.interface'; @@ -40,6 +39,7 @@ import type { Position } from './position.interface'; import type { Product } from './product'; import type { AccessTokenResponse } from './responses/access-token-response.interface'; import type { AccountBalancesResponse } from './responses/account-balances-response.interface'; +import type { AccountsResponse } from './responses/accounts-response.interface'; import type { AiPromptResponse } from './responses/ai-prompt-response.interface'; import type { ApiKeyResponse } from './responses/api-key-response.interface'; import type { BenchmarkResponse } from './responses/benchmark-response.interface'; @@ -74,7 +74,7 @@ export { AccessTokenResponse, AccountBalance, AccountBalancesResponse, - Accounts, + AccountsResponse, AdminData, AdminJobs, AdminMarketData, diff --git a/libs/common/src/lib/interfaces/accounts.interface.ts b/libs/common/src/lib/interfaces/responses/accounts-response.interface.ts similarity index 84% rename from libs/common/src/lib/interfaces/accounts.interface.ts rename to libs/common/src/lib/interfaces/responses/accounts-response.interface.ts index 7100a684..5e03ea34 100644 --- a/libs/common/src/lib/interfaces/accounts.interface.ts +++ b/libs/common/src/lib/interfaces/responses/accounts-response.interface.ts @@ -1,6 +1,6 @@ import { AccountWithValue } from '@ghostfolio/common/types'; -export interface Accounts { +export interface AccountsResponse { accounts: AccountWithValue[]; totalBalanceInBaseCurrency: number; totalValueInBaseCurrency: number; From 1bced964609a2843ecb39449a7c1e69312738aeb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 3 May 2025 20:47:02 +0200 Subject: [PATCH 8/9] Feature/deprecate portfolio position endpoints (#4648) * Deprecate api/v1/portfolio/position endpoints * Update changelog --- CHANGELOG.md | 7 ++ apps/api/src/app/import/import.service.ts | 2 +- .../src/app/portfolio/portfolio.controller.ts | 74 +++++++++++++++++-- .../src/app/portfolio/portfolio.service.ts | 8 +- apps/client/src/app/services/data.service.ts | 8 +- libs/common/src/lib/interfaces/index.ts | 2 + .../portfolio-holding-response.interface.ts | 2 +- 7 files changed, 88 insertions(+), 15 deletions(-) rename apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts => libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9f4a443..95d1e00e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Deprecated the endpoint to get a portfolio position in favor of get a holding +- Deprecated the endpoint to update portfolio position tags in favor of update holding tags + ## 2.159.0 - 2025-05-02 ### Added diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index b6fe0d94..4cb6d46c 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -50,7 +50,7 @@ export class ImportService { }: AssetProfileIdentifier): Promise { try { const { firstBuyDate, historicalData, orders } = - await this.portfolioService.getPosition(dataSource, undefined, symbol); + await this.portfolioService.getHolding(dataSource, undefined, symbol); const [[assetProfile], dividends] = await Promise.all([ this.symbolProfileService.getSymbolProfiles([ diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index c3e46d50..5b68f58e 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -20,6 +20,7 @@ import { import { PortfolioDetails, PortfolioDividends, + PortfolioHoldingResponse, PortfolioHoldingsResponse, PortfolioInvestments, PortfolioPerformanceResponse, @@ -56,7 +57,6 @@ import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; import { Big } from 'big.js'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; -import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface'; import { PortfolioService } from './portfolio.service'; import { UpdateHoldingTagsDto } from './update-holding-tags.dto'; @@ -365,6 +365,32 @@ export class PortfolioController { return { dividends }; } + @Get('holding/:dataSource/:symbol') + @UseInterceptors(RedactValuesInResponseInterceptor) + @UseInterceptors(TransformDataSourceInRequestInterceptor) + @UseInterceptors(TransformDataSourceInResponseInterceptor) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async getHolding( + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string + ): Promise { + const holding = await this.portfolioService.getHolding( + dataSource, + impersonationId, + symbol + ); + + if (!holding) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + return holding; + } + @Get('holdings') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(RedactValuesInResponseInterceptor) @@ -583,6 +609,9 @@ export class PortfolioController { return performanceInformation; } + /** + * @deprecated + */ @Get('position/:dataSource/:symbol') @UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor) @@ -592,8 +621,8 @@ export class PortfolioController { @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string - ): Promise { - const holding = await this.portfolioService.getPosition( + ): Promise { + const holding = await this.portfolioService.getHolding( dataSource, impersonationId, symbol @@ -634,7 +663,7 @@ export class PortfolioController { } @HasPermission(permissions.updateOrder) - @Put('position/:dataSource/:symbol/tags') + @Put('holding/:dataSource/:symbol/tags') @UseInterceptors(TransformDataSourceInRequestInterceptor) @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async updateHoldingTags( @@ -643,7 +672,42 @@ export class PortfolioController { @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { - const holding = await this.portfolioService.getPosition( + const holding = await this.portfolioService.getHolding( + dataSource, + impersonationId, + symbol + ); + + if (!holding) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + await this.portfolioService.updateTags({ + dataSource, + impersonationId, + symbol, + tags: data.tags, + userId: this.request.user.id + }); + } + + /** + * @deprecated + */ + @HasPermission(permissions.updateOrder) + @Put('position/:dataSource/:symbol/tags') + @UseInterceptors(TransformDataSourceInRequestInterceptor) + @UseGuards(AuthGuard('jwt'), HasPermissionGuard) + public async updatePositionTags( + @Body() data: UpdateHoldingTagsDto, + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string + ): Promise { + const holding = await this.portfolioService.getHolding( dataSource, impersonationId, symbol diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index a2480974..26265e8c 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -41,6 +41,7 @@ import { HistoricalDataItem, InvestmentItem, PortfolioDetails, + PortfolioHoldingResponse, PortfolioInvestments, PortfolioPerformanceResponse, PortfolioPosition, @@ -87,7 +88,6 @@ import { isEmpty } from 'lodash'; import { PortfolioCalculator } from './calculator/portfolio-calculator'; import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory'; -import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface'; import { RulesService } from './rules.service'; const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json'); @@ -631,11 +631,11 @@ export class PortfolioService { }; } - public async getPosition( + public async getHolding( aDataSource: DataSource, aImpersonationId: string, aSymbol: string - ): Promise { + ): Promise { const userId = await this.getUserId(aImpersonationId, this.request.user.id); const user = await this.userService.user({ id: userId }); const userCurrency = this.getUserCurrency(user); @@ -927,7 +927,7 @@ export class PortfolioService { } } - public async getPositions({ + public async getHoldings({ dateRange = 'max', filters, impersonationId diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 830543dd..41cde8c8 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -13,7 +13,6 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto'; -import { PortfolioHoldingDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-holding-detail.interface'; import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface'; import { DeleteOwnUserDto } from '@ghostfolio/api/app/user/delete-own-user.dto'; import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface'; @@ -40,6 +39,7 @@ import { OAuthResponse, PortfolioDetails, PortfolioDividends, + PortfolioHoldingResponse, PortfolioHoldingsResponse, PortfolioInvestments, PortfolioPerformanceResponse, @@ -406,8 +406,8 @@ export class DataService { symbol: string; }) { return this.http - .get( - `/api/v1/portfolio/position/${dataSource}/${symbol}` + .get( + `/api/v1/portfolio/holding/${dataSource}/${symbol}` ) .pipe( map((data) => { @@ -776,7 +776,7 @@ export class DataService { tags }: { tags: Tag[] } & AssetProfileIdentifier) { return this.http.put( - `/api/v1/portfolio/position/${dataSource}/${symbol}/tags`, + `/api/v1/portfolio/holding/${dataSource}/${symbol}/tags`, { tags } ); } diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 3e0528fd..bdf982f5 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -52,6 +52,7 @@ import type { ImportResponse } from './responses/import-response.interface'; import type { LookupResponse } from './responses/lookup-response.interface'; import type { MarketDataDetailsResponse } from './responses/market-data-details-response.interface'; import type { OAuthResponse } from './responses/oauth-response.interface'; +import { PortfolioHoldingResponse } from './responses/portfolio-holding-response.interface'; import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface'; import type { PortfolioPerformanceResponse } from './responses/portfolio-performance-response.interface'; import type { PortfolioReportResponse } from './responses/portfolio-report.interface'; @@ -112,6 +113,7 @@ export { PortfolioChart, PortfolioDetails, PortfolioDividends, + PortfolioHoldingResponse, PortfolioHoldingsResponse, PortfolioInvestments, PortfolioItem, diff --git a/apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts similarity index 96% rename from apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts rename to libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts index 79e4d40d..cfdc1611 100644 --- a/apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts +++ b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts @@ -7,7 +7,7 @@ import { import { Tag } from '@prisma/client'; -export interface PortfolioHoldingDetail { +export interface PortfolioHoldingResponse { averagePrice: number; dataProviderInfo: DataProviderInfo; dividendInBaseCurrency: number; From 3646fb7f7769bdce495d995ee6d69c222f3e647a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 4 May 2025 09:48:43 +0200 Subject: [PATCH 9/9] Feature/refactor portfolio holding response (#4649) * Refactor portfolio holding response * maxPrice -> marketPriceMax * minPrice -> marketPriceMin * orders -> activities --- apps/api/src/app/import/import.service.ts | 6 +-- .../src/app/portfolio/portfolio.service.ts | 52 +++++++++++-------- .../holding-detail-dialog.component.ts | 12 ++--- .../holding-detail-dialog.html | 12 ++--- apps/client/src/app/services/data.service.ts | 4 +- .../portfolio-holding-response.interface.ts | 6 +-- 6 files changed, 49 insertions(+), 43 deletions(-) diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 4cb6d46c..7afb2cd7 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -49,7 +49,7 @@ export class ImportService { symbol }: AssetProfileIdentifier): Promise { try { - const { firstBuyDate, historicalData, orders } = + const { activities, firstBuyDate, historicalData } = await this.portfolioService.getHolding(dataSource, undefined, symbol); const [[assetProfile], dividends] = await Promise.all([ @@ -68,7 +68,7 @@ export class ImportService { }) ]); - const accounts = orders + const accounts = activities .filter(({ Account }) => { return !!Account; }) @@ -88,7 +88,7 @@ export class ImportService { const value = new Big(quantity).mul(marketPrice).toNumber(); const date = parseDate(dateString); - const isDuplicate = orders.some((activity) => { + const isDuplicate = activities.some((activity) => { return ( activity.accountId === Account?.id && activity.SymbolProfile.currency === assetProfile.currency && diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 26265e8c..47c773e8 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -648,6 +648,7 @@ export class PortfolioService { if (activities.length === 0) { return { + activities: [], averagePrice: undefined, dataProviderInfo: undefined, dividendInBaseCurrency: undefined, @@ -662,13 +663,12 @@ export class PortfolioService { historicalData: [], investment: undefined, marketPrice: undefined, - maxPrice: undefined, - minPrice: undefined, + marketPriceMax: undefined, + marketPriceMin: undefined, netPerformance: undefined, netPerformancePercent: undefined, netPerformancePercentWithCurrencyEffect: undefined, netPerformanceWithCurrencyEffect: undefined, - orders: [], quantity: undefined, SymbolProfile: undefined, tags: [], @@ -714,7 +714,7 @@ export class PortfolioService { transactionCount } = position; - const activitiesOfPosition = activities.filter(({ SymbolProfile }) => { + const activitiesOfHolding = activities.filter(({ SymbolProfile }) => { return ( SymbolProfile.dataSource === dataSource && SymbolProfile.symbol === symbol @@ -748,12 +748,12 @@ export class PortfolioService { ); const historicalDataArray: HistoricalDataItem[] = []; - let maxPrice = Math.max( - activitiesOfPosition[0].unitPriceInAssetProfileCurrency, + let marketPriceMax = Math.max( + activitiesOfHolding[0].unitPriceInAssetProfileCurrency, marketPrice ); - let minPrice = Math.min( - activitiesOfPosition[0].unitPriceInAssetProfileCurrency, + let marketPriceMin = Math.min( + activitiesOfHolding[0].unitPriceInAssetProfileCurrency, marketPrice ); @@ -793,27 +793,31 @@ export class PortfolioService { quantity: currentQuantity }); - maxPrice = Math.max(marketPrice ?? 0, maxPrice); - minPrice = Math.min(marketPrice ?? Number.MAX_SAFE_INTEGER, minPrice); + marketPriceMax = Math.max(marketPrice ?? 0, marketPriceMax); + marketPriceMin = Math.min( + marketPrice ?? Number.MAX_SAFE_INTEGER, + marketPriceMin + ); } } else { // Add historical entry for buy date, if no historical data available historicalDataArray.push({ - averagePrice: activitiesOfPosition[0].unitPriceInAssetProfileCurrency, + averagePrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, date: firstBuyDate, - marketPrice: activitiesOfPosition[0].unitPriceInAssetProfileCurrency, - quantity: activitiesOfPosition[0].quantity + marketPrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, + quantity: activitiesOfHolding[0].quantity }); } return { firstBuyDate, marketPrice, - maxPrice, - minPrice, + marketPriceMax, + marketPriceMin, SymbolProfile, tags, transactionCount, + activities: activitiesOfHolding, averagePrice: averagePrice.toNumber(), dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), @@ -842,7 +846,6 @@ export class PortfolioService { ]?.toNumber(), netPerformanceWithCurrencyEffect: position.netPerformanceWithCurrencyEffectMap?.['max']?.toNumber(), - orders: activitiesOfPosition, quantity: quantity.toNumber(), value: this.exchangeRateDataService.toCurrency( quantity.mul(marketPrice ?? 0).toNumber(), @@ -881,8 +884,8 @@ export class PortfolioService { } const historicalDataArray: HistoricalDataItem[] = []; - let maxPrice = marketPrice; - let minPrice = marketPrice; + let marketPriceMax = marketPrice; + let marketPriceMin = marketPrice; for (const [date, { marketPrice }] of Object.entries( historicalData[aSymbol] @@ -892,15 +895,19 @@ export class PortfolioService { value: marketPrice }); - maxPrice = Math.max(marketPrice ?? 0, maxPrice); - minPrice = Math.min(marketPrice ?? Number.MAX_SAFE_INTEGER, minPrice); + marketPriceMax = Math.max(marketPrice ?? 0, marketPriceMax); + marketPriceMin = Math.min( + marketPrice ?? Number.MAX_SAFE_INTEGER, + marketPriceMin + ); } return { marketPrice, - maxPrice, - minPrice, + marketPriceMax, + marketPriceMin, SymbolProfile, + activities: [], averagePrice: 0, dataProviderInfo: undefined, dividendInBaseCurrency: 0, @@ -918,7 +925,6 @@ export class PortfolioService { netPerformancePercent: undefined, netPerformancePercentWithCurrencyEffect: undefined, netPerformanceWithCurrencyEffect: undefined, - orders: [], quantity: 0, tags: [], transactionCount: undefined, diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index 25317e0c..925a6442 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -105,8 +105,8 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { public investmentPrecision = 2; public marketDataItems: MarketData[] = []; public marketPrice: number; - public maxPrice: number; - public minPrice: number; + public marketPriceMax: number; + public marketPriceMin: number; public netPerformance: number; public netPerformancePrecision = 2; public netPerformancePercent: number; @@ -234,8 +234,8 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { historicalData, investment, marketPrice, - maxPrice, - minPrice, + marketPriceMax, + marketPriceMin, netPerformance, netPerformancePercent, netPerformancePercentWithCurrencyEffect, @@ -297,8 +297,8 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { } this.marketPrice = marketPrice; - this.maxPrice = maxPrice; - this.minPrice = minPrice; + this.marketPriceMax = marketPriceMax; + this.marketPriceMin = marketPriceMin; this.netPerformance = netPerformance; if ( diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index 9e8855d7..d18dc479 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -106,11 +106,11 @@ [locale]="data.locale" [ngClass]="{ 'text-danger': - minPrice?.toFixed(2) === marketPrice?.toFixed(2) && - maxPrice?.toFixed(2) !== minPrice?.toFixed(2) + marketPriceMin?.toFixed(2) === marketPrice?.toFixed(2) && + marketPriceMax?.toFixed(2) !== marketPriceMin?.toFixed(2) }" [unit]="SymbolProfile?.currency" - [value]="minPrice" + [value]="marketPriceMin" >Minimum Price
@@ -122,11 +122,11 @@ [locale]="data.locale" [ngClass]="{ 'text-success': - maxPrice?.toFixed(2) === marketPrice?.toFixed(2) && - maxPrice?.toFixed(2) !== minPrice?.toFixed(2) + marketPriceMax?.toFixed(2) === marketPrice?.toFixed(2) && + marketPriceMax?.toFixed(2) !== marketPriceMin?.toFixed(2) }" [unit]="SymbolProfile?.currency" - [value]="maxPrice" + [value]="marketPriceMax" >Maximum Price diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 41cde8c8..e3bd9f27 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -411,8 +411,8 @@ export class DataService { ) .pipe( map((data) => { - if (data.orders) { - for (const order of data.orders) { + if (data.activities) { + for (const order of data.activities) { order.createdAt = parseISO(order.createdAt as unknown as string); order.date = parseISO(order.date as unknown as string); } diff --git a/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts index cfdc1611..c460242a 100644 --- a/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/portfolio-holding-response.interface.ts @@ -8,6 +8,7 @@ import { import { Tag } from '@prisma/client'; export interface PortfolioHoldingResponse { + activities: Activity[]; averagePrice: number; dataProviderInfo: DataProviderInfo; dividendInBaseCurrency: number; @@ -22,13 +23,12 @@ export interface PortfolioHoldingResponse { historicalData: HistoricalDataItem[]; investment: number; marketPrice: number; - maxPrice: number; - minPrice: number; + marketPriceMax: number; + marketPriceMin: number; netPerformance: number; netPerformancePercent: number; netPerformancePercentWithCurrencyEffect: number; netPerformanceWithCurrencyEffect: number; - orders: Activity[]; quantity: number; SymbolProfile: EnhancedSymbolProfile; tags: Tag[];