mirror of https://github.com/ghostfolio/ghostfolio
Thomas Kaul
3 years ago
committed by
GitHub
32 changed files with 351 additions and 45 deletions
@ -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 { DataSource } from '@prisma/client'; |
|||
import { HistoricalDataItem, UniqueAsset } from '@ghostfolio/common/interfaces'; |
|||
|
|||
export interface SymbolItem { |
|||
export interface SymbolItem extends UniqueAsset { |
|||
currency: string; |
|||
dataSource: DataSource; |
|||
historicalData: HistoricalDataItem[]; |
|||
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 { Country } from './country.interface'; |
|||
import { ScraperConfiguration } from './scraper-configuration.interface'; |
|||
import { Sector } from './sector.interface'; |
|||
|
|||
export interface EnhancedSymbolProfile { |
|||
assetClass: AssetClass; |
|||
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