Browse Source

Extend markets

pull/5076/head
Thomas Kaul 2 months ago
parent
commit
e15360636f
  1. 52
      apps/api/src/app/endpoints/market-data/market-data.controller.ts
  2. 8
      apps/api/src/app/endpoints/market-data/market-data.module.ts
  3. 141
      apps/client/src/app/components/markets/markets.component.ts
  4. 64
      apps/client/src/app/components/markets/markets.html
  5. 7
      apps/client/src/app/components/markets/markets.scss
  6. 6
      apps/client/src/app/pages/home/home-page-routing.module.ts
  7. 5
      apps/client/src/app/pages/home/home-page.component.ts
  8. 27
      apps/client/src/app/services/data.service.ts
  9. 2
      libs/common/src/lib/interfaces/index.ts
  10. 6
      libs/common/src/lib/interfaces/responses/market-data-of-markets-response.interface.ts
  11. 2
      libs/common/src/lib/permissions.ts
  12. 1
      libs/common/src/lib/types/fear-and-greed-index.type.ts
  13. 2
      libs/common/src/lib/types/index.ts

52
apps/api/src/app/endpoints/market-data/market-data.controller.ts

@ -1,8 +1,18 @@
import { AdminService } from '@ghostfolio/api/app/admin/admin.service'; import { AdminService } from '@ghostfolio/api/app/admin/admin.service';
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
ghostfolioFearAndGreedIndexDataSource,
ghostfolioFearAndGreedIndexSymbol
} from '@ghostfolio/common/config';
import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper'; import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
import { MarketDataDetailsResponse } from '@ghostfolio/common/interfaces'; import {
MarketDataDetailsResponse,
MarketDataOfMarketsResponse
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { RequestWithUser } from '@ghostfolio/common/types'; import { RequestWithUser } from '@ghostfolio/common/types';
@ -14,6 +24,7 @@ import {
Inject, Inject,
Param, Param,
Post, Post,
Query,
UseGuards UseGuards
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
@ -30,9 +41,46 @@ export class MarketDataController {
private readonly adminService: AdminService, private readonly adminService: AdminService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService,
private readonly symbolService: SymbolService
) {} ) {}
@Get('markets')
@HasPermission(permissions.readMarketDataOfMarkets)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getMarketDataOfMarkets(
@Query('includeHistoricalData') includeHistoricalData = 0
): Promise<MarketDataOfMarketsResponse> {
const [
marketDataFearAndGreedIndexCryptocurrencies,
marketDataFearAndGreedIndexStocks
] = await Promise.all([
this.symbolService.get({
includeHistoricalData,
dataGatheringItem: {
dataSource: 'MANUAL',
symbol: 'GF_FEAR_AND_GREED_INDEX_CRYPTO'
}
}),
this.symbolService.get({
includeHistoricalData,
dataGatheringItem: {
dataSource: ghostfolioFearAndGreedIndexDataSource,
symbol: ghostfolioFearAndGreedIndexSymbol
}
})
]);
return {
CRYPTOCURRENCIES: {
...marketDataFearAndGreedIndexCryptocurrencies
},
STOCKS: {
...marketDataFearAndGreedIndexStocks
}
};
}
@Get(':dataSource/:symbol') @Get(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async getMarketDataBySymbol( public async getMarketDataBySymbol(

8
apps/api/src/app/endpoints/market-data/market-data.module.ts

@ -1,4 +1,5 @@
import { AdminModule } from '@ghostfolio/api/app/admin/admin.module'; import { AdminModule } from '@ghostfolio/api/app/admin/admin.module';
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -8,6 +9,11 @@ import { MarketDataController } from './market-data.controller';
@Module({ @Module({
controllers: [MarketDataController], controllers: [MarketDataController],
imports: [AdminModule, MarketDataServiceModule, SymbolProfileModule] imports: [
AdminModule,
MarketDataServiceModule,
SymbolModule,
SymbolProfileModule
]
}) })
export class MarketDataModule {} export class MarketDataModule {}

141
apps/client/src/app/components/markets/markets.component.ts

@ -0,0 +1,141 @@
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,
InfoItem,
MarketDataOfMarketsResponse,
ToggleOption,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
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;
public fearLabel = $localize`Fear`;
public greedLabel = $localize`Greed`;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public historicalDataItems: HistoricalDataItem[];
public fearAndGreedIndexMode: FearAndGreedIndexMode = 'STOCKS';
public fearAndGreedIndexModeOptions: ToggleOption[] = [
{ label: $localize`Stocks`, value: 'STOCKS' },
{ label: $localize`Cryptocurrencies`, value: 'CRYPTOCURRENCIES' }
];
public info: InfoItem;
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.info = this.dataService.fetchInfo();
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnInit() {
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchMarketDataOfMarkets({ includeHistoricalData: this.numberOfDays })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((fearAndGreedIndexData) => {
this.fearAndGreedIndexData = fearAndGreedIndexData;
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
}
];
}
// TODO
public onChangeFearAndGreedIndexMode(
aFearAndGreedIndexMode: FearAndGreedIndexMode
) {
this.fearAndGreedIndexMode = aFearAndGreedIndexMode;
this.initialize();
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

64
apps/client/src/app/components/markets/markets.html

@ -0,0 +1,64 @@
<div class="container">
<h1 class="d-none d-sm-block h3 mb-4 text-center" i18n>Markets</h1>
@if (hasPermissionToAccessFearAndGreedIndex) {
<div class="mb-5 row">
<div class="col-xs-12 col-md-10 offset-md-1">
<div class="d-flex">
<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>
<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>

7
apps/client/src/app/components/markets/markets.scss

@ -0,0 +1,7 @@
:host {
display: block;
gf-line-chart {
aspect-ratio: 16 / 9;
}
}

6
apps/client/src/app/pages/home/home-page-routing.module.ts

@ -3,6 +3,7 @@ import { HomeMarketComponent } from '@ghostfolio/client/components/home-market/h
import { HomeOverviewComponent } from '@ghostfolio/client/components/home-overview/home-overview.component'; import { HomeOverviewComponent } from '@ghostfolio/client/components/home-overview/home-overview.component';
import { HomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component'; import { HomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component';
import { HomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component'; import { HomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component';
import { MarketsComponent } from '@ghostfolio/client/components/markets/markets.component';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
@ -34,6 +35,11 @@ const routes: Routes = [
component: HomeMarketComponent, component: HomeMarketComponent,
title: internalRoutes.home.subRoutes.markets.title title: internalRoutes.home.subRoutes.markets.title
}, },
{
path: internalRoutes.home.subRoutes.markets.path + '-new', // TODO
component: MarketsComponent,
title: internalRoutes.home.subRoutes.markets.title
},
{ {
path: internalRoutes.home.subRoutes.watchlist.path, path: internalRoutes.home.subRoutes.watchlist.path,
component: HomeWatchlistComponent, component: HomeWatchlistComponent,

5
apps/client/src/app/pages/home/home-page.component.ts

@ -58,6 +58,11 @@ export class HomePageComponent implements OnDestroy, OnInit {
iconName: 'newspaper-outline', iconName: 'newspaper-outline',
label: internalRoutes.home.subRoutes.markets.title, label: internalRoutes.home.subRoutes.markets.title,
routerLink: internalRoutes.home.subRoutes.markets.routerLink routerLink: internalRoutes.home.subRoutes.markets.routerLink
},
{
iconName: 'newspaper-outline',
label: internalRoutes.home.subRoutes.markets.title + ' (new)',
routerLink: ['/home', 'markets-new'] // TODO
} }
]; ];

27
apps/client/src/app/services/data.service.ts

@ -38,6 +38,7 @@ import {
InfoItem, InfoItem,
LookupResponse, LookupResponse,
MarketDataDetailsResponse, MarketDataDetailsResponse,
MarketDataOfMarketsResponse,
OAuthResponse, OAuthResponse,
PortfolioDetails, PortfolioDetails,
PortfolioDividends, PortfolioDividends,
@ -483,6 +484,32 @@ export class DataService {
); );
} }
public fetchMarketDataOfMarkets({
includeHistoricalData
}: {
includeHistoricalData?: number;
}): Observable<MarketDataOfMarketsResponse> {
let params = new HttpParams();
if (includeHistoricalData) {
params = params.append('includeHistoricalData', includeHistoricalData);
}
return this.http.get<any>('/api/v1/market-data/markets', { params }).pipe(
map((data) => {
for (const item of data.CRYPTOCURRENCIES.historicalData) {
item.date = parseISO(item.date);
}
for (const item of data.STOCKS.historicalData) {
item.date = parseISO(item.date);
}
return data;
})
);
}
public fetchSymbolItem({ public fetchSymbolItem({
dataSource, dataSource,
includeHistoricalData, includeHistoricalData,

2
libs/common/src/lib/interfaces/index.ts

@ -51,6 +51,7 @@ import type { HistoricalResponse } from './responses/historical-response.interfa
import type { ImportResponse } from './responses/import-response.interface'; import type { ImportResponse } from './responses/import-response.interface';
import type { LookupResponse } from './responses/lookup-response.interface'; import type { LookupResponse } from './responses/lookup-response.interface';
import type { MarketDataDetailsResponse } from './responses/market-data-details-response.interface'; import type { MarketDataDetailsResponse } from './responses/market-data-details-response.interface';
import type { MarketDataOfMarketsResponse } from './responses/market-data-of-markets-response.interface';
import type { OAuthResponse } from './responses/oauth-response.interface'; import type { OAuthResponse } from './responses/oauth-response.interface';
import { PortfolioHoldingResponse } from './responses/portfolio-holding-response.interface'; import { PortfolioHoldingResponse } from './responses/portfolio-holding-response.interface';
import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface'; import type { PortfolioHoldingsResponse } from './responses/portfolio-holdings-response.interface';
@ -111,6 +112,7 @@ export {
LookupItem, LookupItem,
LookupResponse, LookupResponse,
MarketDataDetailsResponse, MarketDataDetailsResponse,
MarketDataOfMarketsResponse,
OAuthResponse, OAuthResponse,
PortfolioChart, PortfolioChart,
PortfolioDetails, PortfolioDetails,

6
libs/common/src/lib/interfaces/responses/market-data-of-markets-response.interface.ts

@ -0,0 +1,6 @@
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
export interface MarketDataOfMarketsResponse {
CRYPTOCURRENCIES: SymbolItem;
STOCKS: SymbolItem;
}

2
libs/common/src/lib/permissions.ts

@ -40,6 +40,7 @@ export const permissions = {
impersonateAllUsers: 'impersonateAllUsers', impersonateAllUsers: 'impersonateAllUsers',
readAiPrompt: 'readAiPrompt', readAiPrompt: 'readAiPrompt',
readMarketData: 'readMarketData', readMarketData: 'readMarketData',
readMarketDataOfMarkets: 'readMarketDataOfMarkets', // TODO
readMarketDataOfOwnAssetProfile: 'readMarketDataOfOwnAssetProfile', readMarketDataOfOwnAssetProfile: 'readMarketDataOfOwnAssetProfile',
readPlatforms: 'readPlatforms', readPlatforms: 'readPlatforms',
readTags: 'readTags', readTags: 'readTags',
@ -87,6 +88,7 @@ export function getPermissions(aRole: Role): string[] {
permissions.deleteUser, permissions.deleteUser,
permissions.readAiPrompt, permissions.readAiPrompt,
permissions.readMarketData, permissions.readMarketData,
permissions.readMarketDataOfMarkets,
permissions.readMarketDataOfOwnAssetProfile, permissions.readMarketDataOfOwnAssetProfile,
permissions.readPlatforms, permissions.readPlatforms,
permissions.readTags, permissions.readTags,

1
libs/common/src/lib/types/fear-and-greed-index.type.ts

@ -0,0 +1 @@
export type FearAndGreedIndexMode = 'CRYPTOCURRENCIES' | 'STOCKS';

2
libs/common/src/lib/types/index.ts

@ -6,6 +6,7 @@ import type { AiPromptMode } from './ai-prompt-mode.type';
import type { BenchmarkTrend } from './benchmark-trend.type'; import type { BenchmarkTrend } from './benchmark-trend.type';
import type { ColorScheme } from './color-scheme.type'; import type { ColorScheme } from './color-scheme.type';
import type { DateRange } from './date-range.type'; import type { DateRange } from './date-range.type';
import type { FearAndGreedIndexMode } from './fear-and-greed-index.type';
import type { Granularity } from './granularity.type'; import type { Granularity } from './granularity.type';
import type { GroupBy } from './group-by.type'; import type { GroupBy } from './group-by.type';
import type { HoldingType } from './holding-type.type'; import type { HoldingType } from './holding-type.type';
@ -30,6 +31,7 @@ export type {
BenchmarkTrend, BenchmarkTrend,
ColorScheme, ColorScheme,
DateRange, DateRange,
FearAndGreedIndexMode,
Granularity, Granularity,
GroupBy, GroupBy,
HoldingType, HoldingType,

Loading…
Cancel
Save