diff --git a/CHANGELOG.md b/CHANGELOG.md index 89e9fa52..177f9e82 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 endpoint to get a holding by the date of the last all time high and the current change to the all time high + ### Changed - Improved the language localization for Turkish (`tr`) diff --git a/apps/api/src/app/endpoints/ai/ai.module.ts b/apps/api/src/app/endpoints/ai/ai.module.ts index 584f2995..b6f9941a 100644 --- a/apps/api/src/app/endpoints/ai/ai.module.ts +++ b/apps/api/src/app/endpoints/ai/ai.module.ts @@ -8,6 +8,7 @@ import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module'; +import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; @@ -27,6 +28,7 @@ import { AiService } from './ai.service'; controllers: [AiController], imports: [ ApiModule, + BenchmarkModule, ConfigurationModule, DataProviderModule, ExchangeRateDataModule, diff --git a/apps/api/src/app/endpoints/public/public.module.ts b/apps/api/src/app/endpoints/public/public.module.ts index 9b43522c..cf4fd3d1 100644 --- a/apps/api/src/app/endpoints/public/public.module.ts +++ b/apps/api/src/app/endpoints/public/public.module.ts @@ -9,6 +9,7 @@ import { RulesService } from '@ghostfolio/api/app/portfolio/rules.service'; import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; +import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; @@ -25,6 +26,7 @@ import { PublicController } from './public.controller'; controllers: [PublicController], imports: [ AccessModule, + BenchmarkModule, DataProviderModule, ExchangeRateDataModule, ImpersonationModule, diff --git a/apps/api/src/app/portfolio/portfolio.module.ts b/apps/api/src/app/portfolio/portfolio.module.ts index 7ae74ee5..0f64b2f6 100644 --- a/apps/api/src/app/portfolio/portfolio.module.ts +++ b/apps/api/src/app/portfolio/portfolio.module.ts @@ -9,6 +9,7 @@ import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redac 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 { ApiModule } from '@ghostfolio/api/services/api/api.module'; +import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; @@ -33,6 +34,7 @@ import { RulesService } from './rules.service'; imports: [ AccessModule, ApiModule, + BenchmarkModule, ConfigurationModule, DataGatheringModule, DataProviderModule, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 47c773e8..c580ce14 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -20,6 +20,7 @@ import { RegionalMarketClusterRiskEmergingMarkets } from '@ghostfolio/api/models import { RegionalMarketClusterRiskEurope } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/europe'; import { RegionalMarketClusterRiskJapan } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/japan'; import { RegionalMarketClusterRiskNorthAmerica } from '@ghostfolio/api/models/rules/regional-market-cluster-risk/north-america'; +import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; @@ -100,6 +101,7 @@ export class PortfolioService { public constructor( private readonly accountBalanceService: AccountBalanceService, private readonly accountService: AccountService, + private readonly benchmarkService: BenchmarkService, private readonly calculatorFactory: PortfolioCalculatorFactory, private readonly dataProviderService: DataProviderService, private readonly exchangeRateDataService: ExchangeRateDataService, @@ -669,6 +671,7 @@ export class PortfolioService { netPerformancePercent: undefined, netPerformancePercentWithCurrencyEffect: undefined, netPerformanceWithCurrencyEffect: undefined, + performances: undefined, quantity: undefined, SymbolProfile: undefined, tags: [], @@ -752,6 +755,10 @@ export class PortfolioService { activitiesOfHolding[0].unitPriceInAssetProfileCurrency, marketPrice ); + let marketPriceMaxDate = + marketPrice > activitiesOfHolding[0].unitPriceInAssetProfileCurrency + ? new Date() + : activitiesOfHolding[0].date; let marketPriceMin = Math.min( activitiesOfHolding[0].unitPriceInAssetProfileCurrency, marketPrice @@ -793,7 +800,10 @@ export class PortfolioService { quantity: currentQuantity }); - marketPriceMax = Math.max(marketPrice ?? 0, marketPriceMax); + if (marketPrice > marketPriceMax) { + marketPriceMax = marketPrice; + marketPriceMaxDate = parseISO(date); + } marketPriceMin = Math.min( marketPrice ?? Number.MAX_SAFE_INTEGER, marketPriceMin @@ -809,6 +819,12 @@ export class PortfolioService { }); } + const performancePercent = + this.benchmarkService.calculateChangeInPercentage( + marketPriceMax, + marketPrice + ); + return { firstBuyDate, marketPrice, @@ -846,6 +862,12 @@ export class PortfolioService { ]?.toNumber(), netPerformanceWithCurrencyEffect: position.netPerformanceWithCurrencyEffectMap?.['max']?.toNumber(), + performances: { + allTimeHigh: { + performancePercent, + date: marketPriceMaxDate + } + }, quantity: quantity.toNumber(), value: this.exchangeRateDataService.toCurrency( quantity.mul(marketPrice ?? 0).toNumber(), @@ -885,6 +907,7 @@ export class PortfolioService { const historicalDataArray: HistoricalDataItem[] = []; let marketPriceMax = marketPrice; + let marketPriceMaxDate = new Date(); let marketPriceMin = marketPrice; for (const [date, { marketPrice }] of Object.entries( @@ -895,13 +918,22 @@ export class PortfolioService { value: marketPrice }); - marketPriceMax = Math.max(marketPrice ?? 0, marketPriceMax); + if (marketPrice > marketPriceMax) { + marketPriceMax = marketPrice; + marketPriceMaxDate = parseISO(date); + } marketPriceMin = Math.min( marketPrice ?? Number.MAX_SAFE_INTEGER, marketPriceMin ); } + const performancePercent = + this.benchmarkService.calculateChangeInPercentage( + marketPriceMax, + marketPrice + ); + return { marketPrice, marketPriceMax, @@ -925,6 +957,12 @@ export class PortfolioService { netPerformancePercent: undefined, netPerformancePercentWithCurrencyEffect: undefined, netPerformanceWithCurrencyEffect: undefined, + performances: { + allTimeHigh: { + performancePercent, + date: marketPriceMaxDate + } + }, quantity: 0, tags: [], transactionCount: undefined, 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 c460242a..b330e8ba 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 @@ -1,5 +1,6 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { + Benchmark, DataProviderInfo, EnhancedSymbolProfile, HistoricalDataItem @@ -29,6 +30,7 @@ export interface PortfolioHoldingResponse { netPerformancePercent: number; netPerformancePercentWithCurrencyEffect: number; netPerformanceWithCurrencyEffect: number; + performances: Benchmark['performances']; quantity: number; SymbolProfile: EnhancedSymbolProfile; tags: Tag[];