Feature/add historical data chart of fear and greed index (#515)

* Add historical data chart of market mood

* Update changelog
pull/516/head
Thomas Kaul 3 years ago committed by GitHub
parent 563f354e7e
commit 3e82de6b21
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 historical data chart of the _Fear & Greed Index_ (market mood)
### Changed
- Improved the historical data view in the admin control panel (hide invalid and future dates)

@ -1,7 +1,9 @@
import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
import { DataSource } from '@prisma/client';
export interface SymbolItem {
currency: string;
dataSource: DataSource;
historicalData: HistoricalDataItem[];
marketPrice: number;
}

@ -1,10 +1,12 @@
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
DefaultValuePipe,
Get,
HttpException,
Inject,
Param,
ParseBoolPipe,
Query,
UseGuards
} from '@nestjs/common';
@ -51,7 +53,9 @@ export class SymbolController {
@UseGuards(AuthGuard('jwt'))
public async getSymbolData(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
@Param('symbol') symbol: string,
@Query('includeHistoricalData', new DefaultValuePipe(false), ParseBoolPipe)
includeHistoricalData: boolean
): Promise<SymbolItem> {
if (!DataSource[dataSource]) {
throw new HttpException(
@ -60,7 +64,10 @@ export class SymbolController {
);
}
const result = await this.symbolService.get({ dataSource, symbol });
const result = await this.symbolService.get({
includeHistoricalData,
dataGatheringItem: { dataSource, symbol }
});
if (!result || isEmpty(result)) {
throw new HttpException(

@ -1,5 +1,6 @@
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 { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { Module } from '@nestjs/common';
@ -7,7 +8,12 @@ import { SymbolController } from './symbol.controller';
import { SymbolService } from './symbol.service';
@Module({
imports: [ConfigurationModule, DataProviderModule, PrismaModule],
imports: [
ConfigurationModule,
DataProviderModule,
MarketDataModule,
PrismaModule
],
controllers: [SymbolController],
providers: [SymbolService]
})

@ -1,8 +1,11 @@
import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { subDays } from 'date-fns';
import { LookupItem } from './interfaces/lookup-item.interface';
import { SymbolItem } from './interfaces/symbol-item.interface';
@ -11,16 +14,42 @@ import { SymbolItem } from './interfaces/symbol-item.interface';
export class SymbolService {
public constructor(
private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService
) {}
public async get(dataGatheringItem: IDataGatheringItem): Promise<SymbolItem> {
public async get({
dataGatheringItem,
includeHistoricalData = false
}: {
dataGatheringItem: IDataGatheringItem;
includeHistoricalData?: boolean;
}): Promise<SymbolItem> {
const response = await this.dataProviderService.get([dataGatheringItem]);
const { currency, marketPrice } = response[dataGatheringItem.symbol] ?? {};
if (dataGatheringItem.dataSource && marketPrice) {
let historicalData: HistoricalDataItem[];
if (includeHistoricalData) {
const days = 7;
const marketData = await this.marketDataService.getRange({
dateQuery: { gte: subDays(new Date(), days) },
symbols: [dataGatheringItem.symbol]
});
historicalData = marketData.map(({ date, marketPrice }) => {
return {
date: date.toISOString(),
value: marketPrice
};
});
}
return {
currency,
historicalData,
marketPrice,
dataSource: dataGatheringItem.dataSource
};

@ -1,13 +1,13 @@
<div class="align-items-center d-flex flex-row">
<div class="h3 mb-0 mr-2">{{ fearAndGreedIndexEmoji }}</div>
<div class="h2 mb-0 mr-2">{{ fearAndGreedIndexEmoji }}</div>
<div>
<div class="h3 mb-0">
<div class="h4 mb-0">
<span class="mr-2">{{ fearAndGreedIndexText }}</span>
<small class="text-muted"
><strong>{{ fearAndGreedIndex }}</strong
>/100</small
>
</div>
<small class="d-block" i18n>Market Mood</small>
<small class="d-block" i18n>Current Market Mood</small>
</div>
</div>

@ -1,7 +1,9 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { HistoricalDataItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
import { resetHours } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DataSource } from '@prisma/client';
@ -16,6 +18,7 @@ import { takeUntil } from 'rxjs/operators';
export class HomeMarketComponent implements OnDestroy, OnInit {
public fearAndGreedIndex: number;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public historicalData: HistoricalDataItem[];
public isLoading = true;
public user: User;
@ -46,11 +49,19 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
this.dataService
.fetchSymbolItem({
dataSource: DataSource.RAKUTEN,
includeHistoricalData: true,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketPrice }) => {
.subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.historicalData = [
...historicalData,
{
date: resetHours(new Date()).toISOString(),
value: marketPrice
}
];
this.isLoading = false;
this.changeDetectorRef.markForCheck();

@ -9,17 +9,26 @@
w-100
"
>
<div class="row w-100">
<div class="no-gutters row w-100">
<div class="col-xs-12 col-md-8 offset-md-2">
<mat-card class="h-100">
<mat-card-content>
<gf-fear-and-greed-index
class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex"
[hidden]="isLoading"
></gf-fear-and-greed-index>
</mat-card-content>
</mat-card>
<div class="mb-2 text-center text-muted">
<small i18n>Last 7 Days</small>
</div>
<gf-line-chart
class="mb-5"
yMax="100"
yMaxLabel="Greed"
yMin="0"
yMinLabel="Fear"
[historicalDataItems]="historicalData"
[showXAxis]="true"
[showYAxis]="true"
></gf-line-chart>
<gf-fear-and-greed-index
class="d-flex justify-content-center"
[fearAndGreedIndex]="fearAndGreedIndex"
[hidden]="isLoading"
></gf-fear-and-greed-index>
</div>
</div>
</div>

@ -1,14 +1,14 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { HomeMarketComponent } from './home-market.component';
@NgModule({
declarations: [HomeMarketComponent],
exports: [],
imports: [CommonModule, GfFearAndGreedIndexModule, MatCardModule],
imports: [CommonModule, GfFearAndGreedIndexModule, GfLineChartModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})

@ -2,4 +2,8 @@
:host {
display: block;
gf-line-chart {
aspect-ratio: 16 / 9;
}
}

@ -127,12 +127,16 @@ export class DataService {
public fetchSymbolItem({
dataSource,
includeHistoricalData = false,
symbol
}: {
dataSource: DataSource;
includeHistoricalData?: boolean;
symbol: string;
}) {
return this.http.get<SymbolItem>(`/api/symbol/${dataSource}/${symbol}`);
return this.http.get<SymbolItem>(`/api/symbol/${dataSource}/${symbol}`, {
params: { includeHistoricalData }
});
}
public fetchPositions({

@ -43,6 +43,10 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input() showXAxis = false;
@Input() showYAxis = false;
@Input() symbol: string;
@Input() yMax: number;
@Input() yMaxLabel: string;
@Input() yMin: number;
@Input() yMinLabel: string;
@ViewChild('chartCanvas') chartCanvas;
@ -170,11 +174,22 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
grid: {
display: false
},
max: this.yMax,
min: this.yMin,
ticks: {
display: this.showYAxis,
callback: function (tickValue, index, ticks) {
callback: (tickValue, index, ticks) => {
if (index === 0 || index === ticks.length - 1) {
// Only print last and first legend entry
if (index === 0 && this.yMinLabel) {
return this.yMinLabel;
}
if (index === ticks.length - 1 && this.yMaxLabel) {
return this.yMaxLabel;
}
if (typeof tickValue === 'number') {
return tickValue.toFixed(2);
}

Loading…
Cancel
Save