Feature/extend markets overview by benchmarks (#953)
* Add benchmarks to markets overview * Update changelogpull/955/head
parent
4711b0d1ed
commit
2c4c16ec99
@ -0,0 +1,32 @@
|
|||||||
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||||
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||||
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
|
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
|
||||||
|
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
import { Controller, Get, UseGuards, UseInterceptors } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
import { BenchmarkService } from './benchmark.service';
|
||||||
|
|
||||||
|
@Controller('benchmark')
|
||||||
|
export class BenchmarkController {
|
||||||
|
public constructor(
|
||||||
|
private readonly benchmarkService: BenchmarkService,
|
||||||
|
private readonly propertyService: PropertyService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
@UseInterceptors(TransformDataSourceInRequestInterceptor)
|
||||||
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
|
public async getBenchmark(): Promise<BenchmarkResponse> {
|
||||||
|
const benchmarkAssets: UniqueAsset[] =
|
||||||
|
((await this.propertyService.getByKey(
|
||||||
|
PROPERTY_BENCHMARKS
|
||||||
|
)) as UniqueAsset[]) ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
benchmarks: await this.benchmarkService.getBenchmarks(benchmarkAssets)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.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';
|
||||||
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
|
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { BenchmarkController } from './benchmark.controller';
|
||||||
|
import { BenchmarkService } from './benchmark.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [BenchmarkController],
|
||||||
|
imports: [
|
||||||
|
ConfigurationModule,
|
||||||
|
DataProviderModule,
|
||||||
|
MarketDataModule,
|
||||||
|
PropertyModule,
|
||||||
|
RedisCacheModule,
|
||||||
|
SymbolProfileModule
|
||||||
|
],
|
||||||
|
providers: [BenchmarkService]
|
||||||
|
})
|
||||||
|
export class BenchmarkModule {}
|
@ -0,0 +1,77 @@
|
|||||||
|
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
|
||||||
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
|
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||||
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
|
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import Big from 'big.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BenchmarkService {
|
||||||
|
private readonly CACHE_KEY_BENCHMARKS = 'BENCHMARKS';
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly dataProviderService: DataProviderService,
|
||||||
|
private readonly marketDataService: MarketDataService,
|
||||||
|
private readonly redisCacheService: RedisCacheService,
|
||||||
|
private readonly symbolProfileService: SymbolProfileService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async getBenchmarks(
|
||||||
|
benchmarkAssets: UniqueAsset[]
|
||||||
|
): Promise<BenchmarkResponse['benchmarks']> {
|
||||||
|
let benchmarks: BenchmarkResponse['benchmarks'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
benchmarks = JSON.parse(
|
||||||
|
await this.redisCacheService.get(this.CACHE_KEY_BENCHMARKS)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (benchmarks) {
|
||||||
|
return benchmarks;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const promises: Promise<number>[] = [];
|
||||||
|
|
||||||
|
const [quotes, assetProfiles] = await Promise.all([
|
||||||
|
this.dataProviderService.getQuotes(benchmarkAssets),
|
||||||
|
this.symbolProfileService.getSymbolProfiles(benchmarkAssets)
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const benchmarkAsset of benchmarkAssets) {
|
||||||
|
promises.push(this.marketDataService.getMax(benchmarkAsset));
|
||||||
|
}
|
||||||
|
|
||||||
|
const allTimeHighs = await Promise.all(promises);
|
||||||
|
|
||||||
|
benchmarks = allTimeHighs.map((allTimeHigh, index) => {
|
||||||
|
const { marketPrice } = quotes[benchmarkAssets[index].symbol];
|
||||||
|
|
||||||
|
const performancePercentFromAllTimeHigh = new Big(marketPrice)
|
||||||
|
.div(allTimeHigh)
|
||||||
|
.minus(1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: assetProfiles.find(({ dataSource, symbol }) => {
|
||||||
|
return (
|
||||||
|
dataSource === benchmarkAssets[index].dataSource &&
|
||||||
|
symbol === benchmarkAssets[index].symbol
|
||||||
|
);
|
||||||
|
})?.name,
|
||||||
|
performances: {
|
||||||
|
allTimeHigh: {
|
||||||
|
performancePercent: performancePercentFromAllTimeHigh.toNumber()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.redisCacheService.set(
|
||||||
|
this.CACHE_KEY_BENCHMARKS,
|
||||||
|
JSON.stringify(benchmarks)
|
||||||
|
);
|
||||||
|
|
||||||
|
return benchmarks;
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,7 @@
|
|||||||
import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
|
import { HistoricalDataItem, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||||
import { DataSource } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface SymbolItem {
|
export interface SymbolItem extends UniqueAsset {
|
||||||
currency: string;
|
currency: string;
|
||||||
dataSource: DataSource;
|
|
||||||
historicalData: HistoricalDataItem[];
|
historicalData: HistoricalDataItem[];
|
||||||
marketPrice: number;
|
marketPrice: number;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
||||||
|
|
||||||
|
export interface Benchmark {
|
||||||
|
name: EnhancedSymbolProfile['name'];
|
||||||
|
performances: {
|
||||||
|
allTimeHigh: {
|
||||||
|
performancePercent: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
import { ScraperConfiguration } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface';
|
|
||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
|
||||||
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||||
|
|
||||||
|
import { Country } from './country.interface';
|
||||||
|
import { ScraperConfiguration } from './scraper-configuration.interface';
|
||||||
|
import { Sector } from './sector.interface';
|
||||||
|
|
||||||
export interface EnhancedSymbolProfile {
|
export interface EnhancedSymbolProfile {
|
||||||
assetClass: AssetClass;
|
assetClass: AssetClass;
|
||||||
assetSubClass: AssetSubClass;
|
assetSubClass: AssetSubClass;
|
@ -0,0 +1,5 @@
|
|||||||
|
import { Benchmark } from '../benchmark.interface';
|
||||||
|
|
||||||
|
export interface BenchmarkResponse {
|
||||||
|
benchmarks: Benchmark[];
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
<div class="align-items-center d-flex">
|
||||||
|
<div *ngIf="benchmark?.name" class="flex-grow-1 text-truncate">
|
||||||
|
{{ benchmark.name }}
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!benchmark?.name" class="flex-grow-1">
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
animation="pulse"
|
||||||
|
[theme]="{
|
||||||
|
width: '15rem'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
</div>
|
||||||
|
<gf-value
|
||||||
|
class="mx-2"
|
||||||
|
size="medium"
|
||||||
|
[isPercent]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[ngClass]="{
|
||||||
|
'text-danger':
|
||||||
|
benchmark?.performances?.allTimeHigh?.performancePercent < 0,
|
||||||
|
'text-success':
|
||||||
|
benchmark?.performances?.allTimeHigh?.performancePercent > 0
|
||||||
|
}"
|
||||||
|
[value]="
|
||||||
|
benchmark?.performances?.allTimeHigh?.performancePercent ?? undefined
|
||||||
|
"
|
||||||
|
></gf-value>
|
||||||
|
<div class="text-muted">
|
||||||
|
<small class="d-none d-sm-block text-nowrap" i18n>from All Time High</small
|
||||||
|
><small class="d-block d-sm-none text-nowrap" i18n>from ATH</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||||
|
import { Benchmark } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-benchmark',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
templateUrl: './benchmark.component.html',
|
||||||
|
styleUrls: ['./benchmark.component.scss']
|
||||||
|
})
|
||||||
|
export class BenchmarkComponent {
|
||||||
|
@Input() benchmark: Benchmark;
|
||||||
|
@Input() locale: string;
|
||||||
|
|
||||||
|
public constructor() {}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
import { GfValueModule } from '../value';
|
||||||
|
import { BenchmarkComponent } from './benchmark.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [BenchmarkComponent],
|
||||||
|
exports: [BenchmarkComponent],
|
||||||
|
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class GfBenchmarkModule {}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './benchmark.module';
|
Loading…
Reference in new issue