Feature/simplify benchmark configuration (#1248)
* Simplify benchmark configuration * Update changelog
This commit is contained in:
parent
0fcfa6c1bd
commit
e320aa91f7
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Simplified the configuration of the benchmarks: `symbolProfileId` instead of `dataSource` and `symbol`
|
||||||
- Upgraded `yahoo-finance2` from version `2.3.3` to `2.3.6`
|
- Upgraded `yahoo-finance2` from version `2.3.3` to `2.3.6`
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
UniqueAsset
|
UniqueAsset
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
@ -55,25 +56,25 @@ export class BenchmarkService {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const benchmarkAssets: UniqueAsset[] =
|
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles();
|
||||||
((await this.propertyService.getByKey(
|
|
||||||
PROPERTY_BENCHMARKS
|
|
||||||
)) as UniqueAsset[]) ?? [];
|
|
||||||
const promises: Promise<number>[] = [];
|
const promises: Promise<number>[] = [];
|
||||||
|
|
||||||
const [quotes, assetProfiles] = await Promise.all([
|
const quotes = await this.dataProviderService.getQuotes(
|
||||||
this.dataProviderService.getQuotes(benchmarkAssets),
|
benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
|
||||||
this.symbolProfileService.getSymbolProfiles(benchmarkAssets)
|
return { dataSource, symbol };
|
||||||
]);
|
})
|
||||||
|
);
|
||||||
|
|
||||||
for (const benchmarkAsset of benchmarkAssets) {
|
for (const { dataSource, symbol } of benchmarkAssetProfiles) {
|
||||||
promises.push(this.marketDataService.getMax(benchmarkAsset));
|
promises.push(this.marketDataService.getMax({ dataSource, symbol }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const allTimeHighs = await Promise.all(promises);
|
const allTimeHighs = await Promise.all(promises);
|
||||||
|
|
||||||
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||||
const { marketPrice } = quotes[benchmarkAssets[index].symbol] ?? {};
|
const { marketPrice } =
|
||||||
|
quotes[benchmarkAssetProfiles[index].symbol] ?? {};
|
||||||
|
|
||||||
let performancePercentFromAllTimeHigh = 0;
|
let performancePercentFromAllTimeHigh = 0;
|
||||||
|
|
||||||
@ -88,12 +89,7 @@ export class BenchmarkService {
|
|||||||
marketCondition: this.getMarketCondition(
|
marketCondition: this.getMarketCondition(
|
||||||
performancePercentFromAllTimeHigh
|
performancePercentFromAllTimeHigh
|
||||||
),
|
),
|
||||||
name: assetProfiles.find(({ dataSource, symbol }) => {
|
name: benchmarkAssetProfiles[index].name,
|
||||||
return (
|
|
||||||
dataSource === benchmarkAssets[index].dataSource &&
|
|
||||||
symbol === benchmarkAssets[index].symbol
|
|
||||||
);
|
|
||||||
})?.name,
|
|
||||||
performances: {
|
performances: {
|
||||||
allTimeHigh: {
|
allTimeHigh: {
|
||||||
performancePercent: performancePercentFromAllTimeHigh
|
performancePercent: performancePercentFromAllTimeHigh
|
||||||
@ -111,19 +107,23 @@ export class BenchmarkService {
|
|||||||
return benchmarks;
|
return benchmarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBenchmarkAssetProfiles(): Promise<UniqueAsset[]> {
|
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> {
|
||||||
const benchmarkAssets: UniqueAsset[] =
|
const symbolProfileIds: string[] = (
|
||||||
((await this.propertyService.getByKey(
|
((await this.propertyService.getByKey(PROPERTY_BENCHMARKS)) as {
|
||||||
PROPERTY_BENCHMARKS
|
symbolProfileId: string;
|
||||||
)) as UniqueAsset[]) ?? [];
|
}[]) ?? []
|
||||||
|
).map(({ symbolProfileId }) => {
|
||||||
|
return symbolProfileId;
|
||||||
|
});
|
||||||
|
|
||||||
const assetProfiles = await this.symbolProfileService.getSymbolProfiles(
|
const assetProfiles =
|
||||||
benchmarkAssets
|
await this.symbolProfileService.getSymbolProfilesByIds(symbolProfileIds);
|
||||||
);
|
|
||||||
|
|
||||||
return assetProfiles.map(({ dataSource, symbol }) => {
|
return assetProfiles.map(({ dataSource, id, name, symbol }) => {
|
||||||
return {
|
return {
|
||||||
dataSource,
|
dataSource,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
symbol
|
symbol
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
|
||||||
import type { DateRange, ViewMode } from '@ghostfolio/common/types';
|
import type { DateRange, ViewMode } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsIn,
|
IsIn,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsObject,
|
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString
|
IsString
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
@ -14,9 +12,9 @@ export class UpdateUserSettingDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
baseCurrency?: string;
|
baseCurrency?: string;
|
||||||
|
|
||||||
@IsObject()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
benchmark?: UniqueAsset;
|
benchmark?: string;
|
||||||
|
|
||||||
@IsIn(<DateRange[]>['1d', '1y', '5y', 'max', 'ytd'])
|
@IsIn(<DateRange[]>['1d', '1y', '5y', 'max', 'ytd'])
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -102,10 +102,10 @@ export class UserController {
|
|||||||
public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
|
public async updateUserSetting(@Body() data: UpdateUserSettingDto) {
|
||||||
if (
|
if (
|
||||||
size(data) === 1 &&
|
size(data) === 1 &&
|
||||||
data.dateRange &&
|
(data.benchmark || data.dateRange) &&
|
||||||
this.request.user.role === 'DEMO'
|
this.request.user.role === 'DEMO'
|
||||||
) {
|
) {
|
||||||
// Allow date range change for demo user
|
// Allow benchmark or date range change for demo user
|
||||||
} else if (
|
} else if (
|
||||||
!hasPermission(
|
!hasPermission(
|
||||||
this.request.user.permissions,
|
this.request.user.permissions,
|
||||||
|
@ -64,6 +64,23 @@ export class SymbolProfileService {
|
|||||||
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getSymbolProfilesByIds(
|
||||||
|
symbolProfileIds: string[]
|
||||||
|
): Promise<EnhancedSymbolProfile[]> {
|
||||||
|
return this.prismaService.symbolProfile
|
||||||
|
.findMany({
|
||||||
|
include: { SymbolProfileOverrides: true },
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: symbolProfileIds.map((symbolProfileId) => {
|
||||||
|
return symbolProfileId;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
|
@ -14,14 +14,13 @@
|
|||||||
<mat-label i18n>Compare with...</mat-label>
|
<mat-label i18n>Compare with...</mat-label>
|
||||||
<mat-select
|
<mat-select
|
||||||
name="benchmark"
|
name="benchmark"
|
||||||
[compareWith]="compareUniqueAssets"
|
|
||||||
[value]="benchmark"
|
[value]="benchmark"
|
||||||
(selectionChange)="onChangeBenchmark($event.value)"
|
(selectionChange)="onChangeBenchmark($event.value)"
|
||||||
>
|
>
|
||||||
<mat-option
|
<mat-option
|
||||||
*ngFor="let currentBenchmark of benchmarks"
|
*ngFor="let symbolProfile of benchmarks"
|
||||||
[value]="currentBenchmark"
|
[value]="symbolProfile.id"
|
||||||
>{{ currentBenchmark.symbol }}</mat-option
|
>{{ symbolProfile.name }}</mat-option
|
||||||
>
|
>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
@ -39,6 +39,7 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
|
import { SymbolProfile } from '@prisma/client';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-benchmark-comparator',
|
selector: 'gf-benchmark-comparator',
|
||||||
@ -48,14 +49,14 @@ import annotationPlugin from 'chartjs-plugin-annotation';
|
|||||||
})
|
})
|
||||||
export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
||||||
@Input() benchmarkDataItems: LineChartItem[] = [];
|
@Input() benchmarkDataItems: LineChartItem[] = [];
|
||||||
@Input() benchmark: UniqueAsset;
|
@Input() benchmark: string;
|
||||||
@Input() benchmarks: UniqueAsset[];
|
@Input() benchmarks: Partial<SymbolProfile>[];
|
||||||
@Input() daysInMarket: number;
|
@Input() daysInMarket: number;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() performanceDataItems: LineChartItem[];
|
@Input() performanceDataItems: LineChartItem[];
|
||||||
@Input() user: User;
|
@Input() user: User;
|
||||||
|
|
||||||
@Output() benchmarkChanged = new EventEmitter<UniqueAsset>();
|
@Output() benchmarkChanged = new EventEmitter<string>();
|
||||||
@Output() dateRangeChanged = new EventEmitter<DateRange>();
|
@Output() dateRangeChanged = new EventEmitter<DateRange>();
|
||||||
|
|
||||||
@ViewChild('chartCanvas') chartCanvas;
|
@ViewChild('chartCanvas') chartCanvas;
|
||||||
@ -85,18 +86,8 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public compareUniqueAssets(
|
public onChangeBenchmark(symbolProfileId: string) {
|
||||||
uniqueAsset1: UniqueAsset,
|
this.benchmarkChanged.next(symbolProfileId);
|
||||||
uniqueAsset2: UniqueAsset
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
uniqueAsset1?.dataSource === uniqueAsset2?.dataSource &&
|
|
||||||
uniqueAsset1?.symbol === uniqueAsset2?.symbol
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public onChangeBenchmark(benchmark: UniqueAsset) {
|
|
||||||
this.benchmarkChanged.next(benchmark);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChangeDateRange(dateRange: DateRange) {
|
public onChangeDateRange(dateRange: DateRange) {
|
||||||
|
@ -5,11 +5,11 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
|
|||||||
import {
|
import {
|
||||||
HistoricalDataItem,
|
HistoricalDataItem,
|
||||||
Position,
|
Position,
|
||||||
UniqueAsset,
|
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import { DateRange, GroupBy, ToggleOption } from '@ghostfolio/common/types';
|
import { DateRange, GroupBy, ToggleOption } from '@ghostfolio/common/types';
|
||||||
|
import { SymbolProfile } from '@prisma/client';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
import { sortBy } from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
@ -24,7 +24,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
})
|
})
|
||||||
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||||
public benchmarkDataItems: HistoricalDataItem[] = [];
|
public benchmarkDataItems: HistoricalDataItem[] = [];
|
||||||
public benchmarks: UniqueAsset[];
|
public benchmarks: Partial<SymbolProfile>[];
|
||||||
public bottom3: Position[];
|
public bottom3: Position[];
|
||||||
public daysInMarket: number;
|
public daysInMarket: number;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
@ -75,9 +75,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChangeBenchmark(benchmark: UniqueAsset) {
|
public onChangeBenchmark(symbolProfileId: string) {
|
||||||
this.dataService
|
this.dataService
|
||||||
.putUserSetting({ benchmark })
|
.putUserSetting({ benchmark: symbolProfileId })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.userService.remove();
|
this.userService.remove();
|
||||||
@ -179,9 +179,15 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
private updateBenchmarkDataItems() {
|
private updateBenchmarkDataItems() {
|
||||||
if (this.user.settings.benchmark) {
|
if (this.user.settings.benchmark) {
|
||||||
|
const { dataSource, symbol } =
|
||||||
|
this.benchmarks.find(({ id }) => {
|
||||||
|
return id === this.user.settings.benchmark;
|
||||||
|
}) ?? {};
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchBenchmarkBySymbol({
|
.fetchBenchmarkBySymbol({
|
||||||
...this.user.settings.benchmark,
|
dataSource,
|
||||||
|
symbol,
|
||||||
startDate: this.firstOrderDate
|
startDate: this.firstOrderDate
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { Tag } from '@prisma/client';
|
import { SymbolProfile, Tag } from '@prisma/client';
|
||||||
|
|
||||||
import { Statistics } from './statistics.interface';
|
import { Statistics } from './statistics.interface';
|
||||||
import { Subscription } from './subscription.interface';
|
import { Subscription } from './subscription.interface';
|
||||||
import { UniqueAsset } from './unique-asset.interface';
|
|
||||||
|
|
||||||
export interface InfoItem {
|
export interface InfoItem {
|
||||||
baseCurrency: string;
|
baseCurrency: string;
|
||||||
benchmarks: UniqueAsset[];
|
benchmarks: Partial<SymbolProfile>[];
|
||||||
currencies: string[];
|
currencies: string[];
|
||||||
demoAuthToken: string;
|
demoAuthToken: string;
|
||||||
fearAndGreedDataSource?: string;
|
fearAndGreedDataSource?: string;
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { DateRange, ViewMode } from '@ghostfolio/common/types';
|
import { DateRange, ViewMode } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
import { UniqueAsset } from './unique-asset.interface';
|
|
||||||
|
|
||||||
export interface UserSettings {
|
export interface UserSettings {
|
||||||
baseCurrency?: string;
|
baseCurrency?: string;
|
||||||
benchmark?: UniqueAsset;
|
benchmark?: string;
|
||||||
dateRange?: DateRange;
|
dateRange?: DateRange;
|
||||||
emergencyFund?: number;
|
emergencyFund?: number;
|
||||||
isExperimentalFeatures?: boolean;
|
isExperimentalFeatures?: boolean;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user