Feature/add date range component to benchmark comparator (#1240)

* Add date range component

* Update changelog
pull/1241/head
Thomas Kaul 2 years ago committed by GitHub
parent fc4bb71184
commit aece76d98f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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,17 +130,32 @@ export class BenchmarkService {
startDate,
symbol
}: { startDate: Date } & UniqueAsset): Promise<BenchmarkMarketDataDetails> {
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;

@ -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.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();
});
}
}
}

@ -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…
Cancel
Save