diff --git a/CHANGELOG.md b/CHANGELOG.md index 984e7b089..85df415b5 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 2addd2de0..a2d32d1d2 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 6ff71ec50..36a498e1d 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 95cb9e5d2..f37f26bfc 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 daf512b34..efad2fef5 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 ef073d331..d290a4a2d 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 @@ } -
+