diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf6f6199..e8fd1d6e7 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 date range component to the benchmark comparator + ### Changed - Improved the mobile layout of the benchmark comparator diff --git a/apps/api/src/app/benchmark/benchmark.module.ts b/apps/api/src/app/benchmark/benchmark.module.ts index fa26a3afd..4c20e61aa 100644 --- a/apps/api/src/app/benchmark/benchmark.module.ts +++ b/apps/api/src/app/benchmark/benchmark.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 { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data.module'; @@ -18,6 +19,7 @@ import { BenchmarkService } from './benchmark.service'; MarketDataModule, PropertyModule, RedisCacheModule, + SymbolModule, SymbolProfileModule ], providers: [BenchmarkService] diff --git a/apps/api/src/app/benchmark/benchmark.service.spec.ts b/apps/api/src/app/benchmark/benchmark.service.spec.ts index 8875c9e1d..833dbcdfc 100644 --- a/apps/api/src/app/benchmark/benchmark.service.spec.ts +++ b/apps/api/src/app/benchmark/benchmark.service.spec.ts @@ -4,7 +4,7 @@ describe('BenchmarkService', () => { let benchmarkService: BenchmarkService; beforeAll(async () => { - benchmarkService = new BenchmarkService(null, null, null, null, null); + benchmarkService = new BenchmarkService(null, null, null, null, null, null); }); it('calculateChangeInPercentage', async () => { diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts index 51fac6212..7987cb5b9 100644 --- a/apps/api/src/app/benchmark/benchmark.service.ts +++ b/apps/api/src/app/benchmark/benchmark.service.ts @@ -1,4 +1,5 @@ import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; @@ -14,6 +15,7 @@ import { Injectable } from '@nestjs/common'; import Big from 'big.js'; import { format } from 'date-fns'; import ms from 'ms'; +import { v4 as uuidv4 } from 'uuid'; @Injectable() export class BenchmarkService { @@ -24,7 +26,8 @@ export class BenchmarkService { private readonly marketDataService: MarketDataService, private readonly propertyService: PropertyService, private readonly redisCacheService: RedisCacheService, - private readonly symbolProfileService: SymbolProfileService + private readonly symbolProfileService: SymbolProfileService, + private readonly symbolService: SymbolService ) {} public calculateChangeInPercentage(baseValue: number, currentValue: number) { @@ -127,17 +130,32 @@ export class BenchmarkService { startDate, symbol }: { startDate: Date } & UniqueAsset): Promise { - const marketDataItems = await this.marketDataService.marketDataItems({ - orderBy: { - date: 'asc' - }, - where: { - dataSource, - symbol, - date: { - gte: startDate + const [currentSymbolItem, marketDataItems] = await Promise.all([ + this.symbolService.get({ + dataGatheringItem: { + dataSource, + symbol } - } + }), + this.marketDataService.marketDataItems({ + orderBy: { + date: 'asc' + }, + where: { + dataSource, + symbol, + date: { + gte: startDate + } + } + }) + ]); + + marketDataItems.push({ + ...currentSymbolItem, + createdAt: new Date(), + date: new Date(), + id: uuidv4() }); const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0; diff --git a/apps/api/src/app/user/update-user-setting.dto.ts b/apps/api/src/app/user/update-user-setting.dto.ts index de3e9d658..7b850238d 100644 --- a/apps/api/src/app/user/update-user-setting.dto.ts +++ b/apps/api/src/app/user/update-user-setting.dto.ts @@ -1,9 +1,11 @@ +import { UniqueAsset } from '@ghostfolio/common/interfaces'; import type { DateRange } from '@ghostfolio/common/types'; import { ViewMode } from '@prisma/client'; import { IsBoolean, IsIn, IsNumber, + IsObject, IsOptional, IsString } from 'class-validator'; @@ -13,6 +15,10 @@ export class UpdateUserSettingDto { @IsString() baseCurrency?: string; + @IsObject() + @IsOptional() + benchmark?: UniqueAsset; + @IsIn(['1d', '1y', '5y', 'max', 'ytd']) @IsOptional() dateRange?: DateRange; diff --git a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html index 101c57835..e8dc9dcdc 100644 --- a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html +++ b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html @@ -14,16 +14,27 @@ Compare with... - {{ - benchmark.symbol - }} + {{ currentBenchmark.symbol }} +
+ +
(); + @Output() dateRangeChanged = new EventEmitter(); @ViewChild('chartCanvas') chartCanvas; - public benchmark: UniqueAsset; public chart: Chart; + public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS; public isLoading = true; public constructor() { @@ -81,8 +85,22 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy { } } - public onChangeBenchmark(aBenchmark: UniqueAsset) { - this.benchmarkChanged.next(aBenchmark); + public compareUniqueAssets( + uniqueAsset1: UniqueAsset, + uniqueAsset2: UniqueAsset + ) { + return ( + uniqueAsset1?.dataSource === uniqueAsset2?.dataSource && + uniqueAsset1?.symbol === uniqueAsset2?.symbol + ); + } + + public onChangeBenchmark(benchmark: UniqueAsset) { + this.benchmarkChanged.next(benchmark); + } + + public onChangeDateRange(dateRange: DateRange) { + this.dateRangeChanged.next(dateRange); } public ngOnDestroy() { diff --git a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.module.ts b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.module.ts index 16440d3a3..bc455ebc2 100644 --- a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.module.ts +++ b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.module.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatSelectModule } from '@angular/material/select'; +import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { BenchmarkComparatorComponent } from './benchmark-comparator.component'; @@ -12,6 +13,7 @@ import { BenchmarkComparatorComponent } from './benchmark-comparator.component'; imports: [ CommonModule, FormsModule, + GfToggleModule, MatSelectModule, NgxSkeletonLoaderModule, ReactiveFormsModule diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index a900a5412..354ed2e2e 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -97,6 +97,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((user) => { this.user = user; + this.changeDetectorRef.markForCheck(); }); }); diff --git a/apps/client/src/app/pages/account/account-page.component.ts b/apps/client/src/app/pages/account/account-page.component.ts index cc475ee17..8a04e2b5e 100644 --- a/apps/client/src/app/pages/account/account-page.component.ts +++ b/apps/client/src/app/pages/account/account-page.component.ts @@ -244,7 +244,7 @@ export class AccountPageComponent implements OnDestroy, OnInit { ) .subscribe(() => { this.snackBarRef = this.snackBar.open( - '✅' + $localize`Coupon code has been redeemed`, + '✅ ' + $localize`Coupon code has been redeemed`, $localize`Reload`, { duration: 3000 diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index 124e46a78..bea5d0a9a 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -9,7 +9,7 @@ import { User } from '@ghostfolio/common/interfaces'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; -import { GroupBy, ToggleOption } from '@ghostfolio/common/types'; +import { DateRange, GroupBy, ToggleOption } from '@ghostfolio/common/types'; import { differenceInDays } from 'date-fns'; import { sortBy } from 'lodash'; import { DeviceDetectorService } from 'ngx-device-detector'; @@ -64,15 +64,76 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { this.hasImpersonationId = !!aId; }); + this.userService.stateChanged + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((state) => { + if (state?.user) { + this.user = state.user; + + this.update(); + } + }); + } + + public onChangeBenchmark(benchmark: UniqueAsset) { this.dataService - .fetchChart({ range: 'max', version: 2 }) + .putUserSetting({ benchmark }) .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ chart }) => { - this.firstOrderDate = new Date(chart?.[0]?.date); - this.performanceDataItems = chart; + .subscribe(() => { + this.userService.remove(); - this.changeDetectorRef.markForCheck(); + this.userService + .get() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((user) => { + this.user = user; + + this.changeDetectorRef.markForCheck(); + }); }); + } + + public onChangeDateRange(dateRange: DateRange) { + this.dataService + .putUserSetting({ dateRange }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.userService.remove(); + + this.userService + .get() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((user) => { + this.user = user; + + this.changeDetectorRef.markForCheck(); + }); + }); + } + + public onChangeGroupBy(aMode: GroupBy) { + this.mode = aMode; + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private update() { + if (this.user.settings.isExperimentalFeatures) { + this.dataService + .fetchChart({ range: this.user?.settings?.dateRange, version: 2 }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ chart }) => { + this.firstOrderDate = new Date(chart?.[0]?.date ?? new Date()); + this.performanceDataItems = chart; + + this.updateBenchmarkDataItems(); + + this.changeDetectorRef.markForCheck(); + }); + } this.dataService .fetchInvestments() @@ -113,43 +174,27 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { this.changeDetectorRef.markForCheck(); }); - this.userService.stateChanged - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((state) => { - if (state?.user) { - this.user = state.user; - - this.changeDetectorRef.markForCheck(); - } - }); + this.changeDetectorRef.markForCheck(); } - public onChangeBenchmark({ dataSource, symbol }: UniqueAsset) { - this.dataService - .fetchBenchmarkBySymbol({ - dataSource, - symbol, - startDate: this.firstOrderDate - }) - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ marketData }) => { - this.benchmarkDataItems = marketData.map(({ date, value }) => { - return { - date, - value - }; - }); - - this.changeDetectorRef.markForCheck(); - }); - } - - public onChangeGroupBy(aMode: GroupBy) { - this.mode = aMode; - } + private updateBenchmarkDataItems() { + if (this.user.settings.benchmark) { + this.dataService + .fetchBenchmarkBySymbol({ + ...this.user.settings.benchmark, + startDate: this.firstOrderDate + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ marketData }) => { + this.benchmarkDataItems = marketData.map(({ date, value }) => { + return { + date, + value + }; + }); - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); + this.changeDetectorRef.markForCheck(); + }); + } } } diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html index 0c09ae916..7b9498a53 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html @@ -4,6 +4,7 @@
diff --git a/libs/common/src/lib/interfaces/user-settings.interface.ts b/libs/common/src/lib/interfaces/user-settings.interface.ts index 5f16cee7e..a88b88192 100644 --- a/libs/common/src/lib/interfaces/user-settings.interface.ts +++ b/libs/common/src/lib/interfaces/user-settings.interface.ts @@ -1,8 +1,11 @@ import { DateRange } from '@ghostfolio/common/types'; import { ViewMode } from '@prisma/client'; +import { UniqueAsset } from './unique-asset.interface'; + export interface UserSettings { baseCurrency?: string; + benchmark?: UniqueAsset; dateRange?: DateRange; emergencyFund?: number; isExperimentalFeatures?: boolean;