mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
19 changed files with 342 additions and 13 deletions
@ -0,0 +1,129 @@ |
|||||
|
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module'; |
||||
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module'; |
||||
|
import { DataService } from '@ghostfolio/client/services/data.service'; |
||||
|
import { UserService } from '@ghostfolio/client/services/user/user.service'; |
||||
|
import { resetHours } from '@ghostfolio/common/helper'; |
||||
|
import { |
||||
|
Benchmark, |
||||
|
HistoricalDataItem, |
||||
|
MarketDataOfMarketsResponse, |
||||
|
ToggleOption, |
||||
|
User |
||||
|
} from '@ghostfolio/common/interfaces'; |
||||
|
import { FearAndGreedIndexMode } from '@ghostfolio/common/types'; |
||||
|
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark'; |
||||
|
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; |
||||
|
|
||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { |
||||
|
ChangeDetectionStrategy, |
||||
|
ChangeDetectorRef, |
||||
|
Component, |
||||
|
CUSTOM_ELEMENTS_SCHEMA, |
||||
|
OnDestroy, |
||||
|
OnInit |
||||
|
} from '@angular/core'; |
||||
|
import { DeviceDetectorService } from 'ngx-device-detector'; |
||||
|
import { Subject } from 'rxjs'; |
||||
|
import { takeUntil } from 'rxjs/operators'; |
||||
|
|
||||
|
@Component({ |
||||
|
changeDetection: ChangeDetectionStrategy.OnPush, |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
GfBenchmarkComponent, |
||||
|
GfFearAndGreedIndexModule, |
||||
|
GfLineChartComponent, |
||||
|
GfToggleModule |
||||
|
], |
||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA], |
||||
|
selector: 'gf-markets', |
||||
|
styleUrls: ['./markets.scss'], |
||||
|
templateUrl: './markets.html' |
||||
|
}) |
||||
|
export class MarketsComponent implements OnDestroy, OnInit { |
||||
|
public benchmarks: Benchmark[]; |
||||
|
public deviceType: string; |
||||
|
public fearAndGreedIndex: number; |
||||
|
public fearAndGreedIndexData: MarketDataOfMarketsResponse['fearAndGreedIndex']; |
||||
|
public fearLabel = $localize`Fear`; |
||||
|
public greedLabel = $localize`Greed`; |
||||
|
public historicalDataItems: HistoricalDataItem[]; |
||||
|
public fearAndGreedIndexMode: FearAndGreedIndexMode = 'STOCKS'; |
||||
|
public fearAndGreedIndexModeOptions: ToggleOption[] = [ |
||||
|
{ label: $localize`Stocks`, value: 'STOCKS' }, |
||||
|
{ label: $localize`Cryptocurrencies`, value: 'CRYPTOCURRENCIES' } |
||||
|
]; |
||||
|
public readonly numberOfDays = 365; |
||||
|
public user: User; |
||||
|
|
||||
|
private unsubscribeSubject = new Subject<void>(); |
||||
|
|
||||
|
public constructor( |
||||
|
private changeDetectorRef: ChangeDetectorRef, |
||||
|
private dataService: DataService, |
||||
|
private deviceService: DeviceDetectorService, |
||||
|
private userService: UserService |
||||
|
) { |
||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType; |
||||
|
|
||||
|
this.userService.stateChanged |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe((state) => { |
||||
|
if (state?.user) { |
||||
|
this.user = state.user; |
||||
|
|
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public ngOnInit() { |
||||
|
this.dataService |
||||
|
.fetchMarketDataOfMarkets({ includeHistoricalData: this.numberOfDays }) |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe(({ fearAndGreedIndex }) => { |
||||
|
this.fearAndGreedIndexData = fearAndGreedIndex; |
||||
|
|
||||
|
this.initialize(); |
||||
|
|
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
}); |
||||
|
|
||||
|
this.dataService |
||||
|
.fetchBenchmarks() |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe(({ benchmarks }) => { |
||||
|
this.benchmarks = benchmarks; |
||||
|
|
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public initialize() { |
||||
|
this.fearAndGreedIndex = |
||||
|
this.fearAndGreedIndexData[this.fearAndGreedIndexMode]?.marketPrice; |
||||
|
|
||||
|
this.historicalDataItems = [ |
||||
|
...(this.fearAndGreedIndexData[this.fearAndGreedIndexMode] |
||||
|
?.historicalData ?? []), |
||||
|
{ |
||||
|
date: resetHours(new Date()).toISOString(), |
||||
|
value: this.fearAndGreedIndex |
||||
|
} |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
public onChangeFearAndGreedIndexMode( |
||||
|
aFearAndGreedIndexMode: FearAndGreedIndexMode |
||||
|
) { |
||||
|
this.fearAndGreedIndexMode = aFearAndGreedIndexMode; |
||||
|
|
||||
|
this.initialize(); |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
this.unsubscribeSubject.next(); |
||||
|
this.unsubscribeSubject.complete(); |
||||
|
} |
||||
|
} |
@ -0,0 +1,60 @@ |
|||||
|
<div class="container"> |
||||
|
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Markets</h1> |
||||
|
<div class="mb-5 row"> |
||||
|
<div class="col-xs-12 col-md-10 offset-md-1"> |
||||
|
@if (user?.settings?.isExperimentalFeatures) { |
||||
|
<div class="align-items-center d-flex flex-grow-1 justify-content-end"> |
||||
|
<gf-toggle |
||||
|
class="d-none d-lg-block" |
||||
|
[defaultValue]="fearAndGreedIndexMode" |
||||
|
[isLoading]="false" |
||||
|
[options]="fearAndGreedIndexModeOptions" |
||||
|
(change)="onChangeFearAndGreedIndexMode($event.value)" |
||||
|
/> |
||||
|
</div> |
||||
|
} |
||||
|
<div class="mb-2 text-center text-muted"> |
||||
|
<small i18n>Last {{ numberOfDays }} Days</small> |
||||
|
</div> |
||||
|
<gf-line-chart |
||||
|
class="mb-3" |
||||
|
symbol="Fear & Greed Index" |
||||
|
[colorScheme]="user?.settings?.colorScheme" |
||||
|
[historicalDataItems]="historicalDataItems" |
||||
|
[isAnimated]="true" |
||||
|
[locale]="user?.settings?.locale || undefined" |
||||
|
[showXAxis]="true" |
||||
|
[showYAxis]="true" |
||||
|
[yMax]="100" |
||||
|
[yMaxLabel]="greedLabel" |
||||
|
[yMin]="0" |
||||
|
[yMinLabel]="fearLabel" |
||||
|
/> |
||||
|
<gf-fear-and-greed-index |
||||
|
class="d-flex justify-content-center" |
||||
|
[fearAndGreedIndex]="fearAndGreedIndex" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="mb-3 row"> |
||||
|
<div class="col-xs-12 col-md-10 offset-md-1"> |
||||
|
<gf-benchmark |
||||
|
[benchmarks]="benchmarks" |
||||
|
[deviceType]="deviceType" |
||||
|
[locale]="user?.settings?.locale || undefined" |
||||
|
[user]="user" |
||||
|
/> |
||||
|
@if (benchmarks?.length > 0) { |
||||
|
<div |
||||
|
class="gf-text-wrap-balance line-height-1 mt-3 text-center text-muted" |
||||
|
> |
||||
|
<small i18n> |
||||
|
Calculations are based on delayed market data and may not be |
||||
|
displayed in real-time.</small |
||||
|
> |
||||
|
</div> |
||||
|
} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
@ -0,0 +1,7 @@ |
|||||
|
:host { |
||||
|
display: block; |
||||
|
|
||||
|
gf-line-chart { |
||||
|
aspect-ratio: 16 / 9; |
||||
|
} |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface'; |
||||
|
|
||||
|
export interface MarketDataOfMarketsResponse { |
||||
|
fearAndGreedIndex: { |
||||
|
CRYPTOCURRENCIES: SymbolItem; |
||||
|
STOCKS: SymbolItem; |
||||
|
}; |
||||
|
} |
@ -0,0 +1 @@ |
|||||
|
export type FearAndGreedIndexMode = 'CRYPTOCURRENCIES' | 'STOCKS'; |
Loading…
Reference in new issue