Feature/add date range component to benchmark comparator (#1240)
* Add date range component * Update changelog
This commit is contained in:
parent
fc4bb71184
commit
aece76d98f
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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 () => {
|
||||
|
@ -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,7 +130,14 @@ export class BenchmarkService {
|
||||
startDate,
|
||||
symbol
|
||||
}: { startDate: Date } & UniqueAsset): Promise<BenchmarkMarketDataDetails> {
|
||||
const marketDataItems = await this.marketDataService.marketDataItems({
|
||||
const [currentSymbolItem, marketDataItems] = await Promise.all([
|
||||
this.symbolService.get({
|
||||
dataGatheringItem: {
|
||||
dataSource,
|
||||
symbol
|
||||
}
|
||||
}),
|
||||
this.marketDataService.marketDataItems({
|
||||
orderBy: {
|
||||
date: 'asc'
|
||||
},
|
||||
@ -138,6 +148,14 @@ export class BenchmarkService {
|
||||
gte: startDate
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
marketDataItems.push({
|
||||
...currentSymbolItem,
|
||||
createdAt: new Date(),
|
||||
date: new Date(),
|
||||
id: uuidv4()
|
||||
});
|
||||
|
||||
const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0;
|
||||
|
@ -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(<DateRange[]>['1d', '1y', '5y', 'max', 'ytd'])
|
||||
@IsOptional()
|
||||
dateRange?: DateRange;
|
||||
|
@ -14,16 +14,27 @@
|
||||
<mat-label i18n>Compare with...</mat-label>
|
||||
<mat-select
|
||||
name="benchmark"
|
||||
[compareWith]="compareUniqueAssets"
|
||||
[value]="benchmark"
|
||||
(selectionChange)="onChangeBenchmark($event.value)"
|
||||
>
|
||||
<mat-option *ngFor="let benchmark of benchmarks" [value]="benchmark">{{
|
||||
benchmark.symbol
|
||||
}}</mat-option>
|
||||
<mat-option
|
||||
*ngFor="let currentBenchmark of benchmarks"
|
||||
[value]="currentBenchmark"
|
||||
>{{ currentBenchmark.symbol }}</mat-option
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center">
|
||||
<gf-toggle
|
||||
[defaultValue]="user?.settings?.dateRange"
|
||||
[isLoading]="isLoading"
|
||||
[options]="dateRangeOptions"
|
||||
(change)="onChangeDateRange($event.value)"
|
||||
></gf-toggle>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
||||
import {
|
||||
getTooltipOptions,
|
||||
getTooltipPositionerMapTop,
|
||||
@ -27,6 +28,7 @@ import {
|
||||
UniqueAsset,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Chart,
|
||||
LineController,
|
||||
@ -46,6 +48,7 @@ import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
})
|
||||
export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
||||
@Input() benchmarkDataItems: LineChartItem[] = [];
|
||||
@Input() benchmark: UniqueAsset;
|
||||
@Input() benchmarks: UniqueAsset[];
|
||||
@Input() daysInMarket: number;
|
||||
@Input() locale: string;
|
||||
@ -53,11 +56,12 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
||||
@Input() user: User;
|
||||
|
||||
@Output() benchmarkChanged = new EventEmitter<UniqueAsset>();
|
||||
@Output() dateRangeChanged = new EventEmitter<DateRange>();
|
||||
|
||||
@ViewChild('chartCanvas') chartCanvas;
|
||||
|
||||
public benchmark: UniqueAsset;
|
||||
public chart: Chart<any>;
|
||||
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() {
|
||||
|
@ -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
|
||||
|
@ -97,6 +97,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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.dataService
|
||||
.fetchChart({ range: 'max', version: 2 })
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ chart }) => {
|
||||
this.firstOrderDate = new Date(chart?.[0]?.date);
|
||||
this.performanceDataItems = chart;
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onChangeBenchmark(benchmark: UniqueAsset) {
|
||||
this.dataService
|
||||
.putUserSetting({ benchmark })
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.userService.remove();
|
||||
|
||||
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,22 +174,14 @@ 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onChangeBenchmark({ dataSource, symbol }: UniqueAsset) {
|
||||
private updateBenchmarkDataItems() {
|
||||
if (this.user.settings.benchmark) {
|
||||
this.dataService
|
||||
.fetchBenchmarkBySymbol({
|
||||
dataSource,
|
||||
symbol,
|
||||
...this.user.settings.benchmark,
|
||||
startDate: this.firstOrderDate
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
@ -143,13 +196,5 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
public onChangeGroupBy(aMode: GroupBy) {
|
||||
this.mode = aMode;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
<div class="col-lg">
|
||||
<gf-benchmark-comparator
|
||||
class="h-100"
|
||||
[benchmark]="user?.settings?.benchmark"
|
||||
[benchmarkDataItems]="benchmarkDataItems"
|
||||
[benchmarks]="benchmarks"
|
||||
[daysInMarket]="daysInMarket"
|
||||
@ -11,6 +12,7 @@
|
||||
[performanceDataItems]="performanceDataItems"
|
||||
[user]="user"
|
||||
(benchmarkChanged)="onChangeBenchmark($event)"
|
||||
(dateRangeChanged)="onChangeDateRange($event)"
|
||||
></gf-benchmark-comparator>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user