Browse Source

Feature/extend markets (#5076)

* Extend markets

* Clean up
release/2.177.0-beta.0
Thomas Kaul 3 weeks ago
committed by GitHub
parent
commit
53127340f4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 56
      apps/api/src/app/endpoints/market-data/market-data.controller.ts
  3. 8
      apps/api/src/app/endpoints/market-data/market-data.module.ts
  4. 7
      apps/api/src/app/info/info.service.ts
  5. 1
      apps/api/src/app/user/user.service.ts
  6. 4
      apps/api/src/services/twitter-bot/twitter-bot.service.ts
  7. 129
      apps/client/src/app/components/markets/markets.component.ts
  8. 60
      apps/client/src/app/components/markets/markets.html
  9. 7
      apps/client/src/app/components/markets/markets.scss
  10. 6
      apps/client/src/app/pages/home/home-page-routing.module.ts
  11. 19
      apps/client/src/app/pages/home/home-page.component.ts
  12. 29
      apps/client/src/app/services/data.service.ts
  13. 6
      libs/common/src/lib/config.ts
  14. 2
      libs/common/src/lib/interfaces/index.ts
  15. 8
      libs/common/src/lib/interfaces/responses/market-data-of-markets-response.interface.ts
  16. 1
      libs/common/src/lib/permissions.ts
  17. 5
      libs/common/src/lib/routes/routes.ts
  18. 1
      libs/common/src/lib/types/fear-and-greed-index.type.ts
  19. 2
      libs/common/src/lib/types/index.ts

4
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Extended the _Fear & Greed Index_ (market mood) in the markets overview by cryptocurrencies (experimental)
### Changed ### Changed
- Made the `getByKey()` function generic in the property service - Made the `getByKey()` function generic in the property service

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

@ -1,8 +1,20 @@
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 {
ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
ghostfolioFearAndGreedIndexDataSourceStocks,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks
} 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 +26,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 +43,48 @@ 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: ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
symbol: ghostfolioFearAndGreedIndexSymbolCryptocurrencies
}
}),
this.symbolService.get({
includeHistoricalData,
dataGatheringItem: {
dataSource: ghostfolioFearAndGreedIndexDataSourceStocks,
symbol: ghostfolioFearAndGreedIndexSymbolStocks
}
})
]);
return {
fearAndGreedIndex: {
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 {}

7
apps/api/src/app/info/info.service.ts

@ -14,7 +14,7 @@ import {
PROPERTY_DEMO_USER_ID, PROPERTY_DEMO_USER_ID,
PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_SLACK_COMMUNITY_USERS, PROPERTY_SLACK_COMMUNITY_USERS,
ghostfolioFearAndGreedIndexDataSource ghostfolioFearAndGreedIndexDataSourceStocks
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
DATE_FORMAT, DATE_FORMAT,
@ -54,10 +54,11 @@ export class InfoService {
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
info.fearAndGreedDataSource = encodeDataSource( info.fearAndGreedDataSource = encodeDataSource(
ghostfolioFearAndGreedIndexDataSource ghostfolioFearAndGreedIndexDataSourceStocks
); );
} else { } else {
info.fearAndGreedDataSource = ghostfolioFearAndGreedIndexDataSource; info.fearAndGreedDataSource =
ghostfolioFearAndGreedIndexDataSourceStocks;
} }
globalPermissions.push(permissions.enableFearAndGreedIndex); globalPermissions.push(permissions.enableFearAndGreedIndex);

1
apps/api/src/app/user/user.service.ts

@ -419,6 +419,7 @@ export class UserService {
if (!hasRole(user, Role.DEMO)) { if (!hasRole(user, Role.DEMO)) {
currentPermissions.push(permissions.createApiKey); currentPermissions.push(permissions.createApiKey);
currentPermissions.push(permissions.enableDataProviderGhostfolio); currentPermissions.push(permissions.enableDataProviderGhostfolio);
currentPermissions.push(permissions.readMarketDataOfMarkets);
currentPermissions.push(permissions.reportDataGlitch); currentPermissions.push(permissions.reportDataGlitch);
} }

4
apps/api/src/services/twitter-bot/twitter-bot.service.ts

@ -2,7 +2,7 @@ import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { import {
ghostfolioFearAndGreedIndexDataSource, ghostfolioFearAndGreedIndexDataSourceStocks,
ghostfolioFearAndGreedIndexSymbol ghostfolioFearAndGreedIndexSymbol
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
@ -44,7 +44,7 @@ export class TwitterBotService {
try { try {
const symbolItem = await this.symbolService.get({ const symbolItem = await this.symbolService.get({
dataGatheringItem: { dataGatheringItem: {
dataSource: ghostfolioFearAndGreedIndexDataSource, dataSource: ghostfolioFearAndGreedIndexDataSourceStocks,
symbol: ghostfolioFearAndGreedIndexSymbol symbol: ghostfolioFearAndGreedIndexSymbol
} }
}); });

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

@ -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();
}
}

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

@ -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>

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.marketsPremium.path,
component: MarketsComponent,
title: internalRoutes.home.subRoutes.marketsPremium.title
},
{ {
path: internalRoutes.home.subRoutes.watchlist.path, path: internalRoutes.home.subRoutes.watchlist.path,
component: HomeWatchlistComponent, component: HomeWatchlistComponent,

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

@ -1,6 +1,7 @@
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { TabConfiguration, User } from '@ghostfolio/common/interfaces'; import { TabConfiguration, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
@ -33,6 +34,8 @@ export class HomePageComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user;
this.tabs = [ this.tabs = [
{ {
iconName: 'analytics-outline', iconName: 'analytics-outline',
@ -56,13 +59,21 @@ export class HomePageComponent implements OnDestroy, OnInit {
}, },
{ {
iconName: 'newspaper-outline', iconName: 'newspaper-outline',
label: internalRoutes.home.subRoutes.markets.title, label: hasPermission(
routerLink: internalRoutes.home.subRoutes.markets.routerLink this.user?.permissions,
permissions.readMarketDataOfMarkets
)
? internalRoutes.home.subRoutes.marketsPremium.title
: internalRoutes.home.subRoutes.markets.title,
routerLink: hasPermission(
this.user?.permissions,
permissions.readMarketDataOfMarkets
)
? internalRoutes.home.subRoutes.marketsPremium.routerLink
: internalRoutes.home.subRoutes.markets.routerLink
} }
]; ];
this.user = state.user;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
}); });

29
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,34 @@ 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.fearAndGreedIndex.CRYPTOCURRENCIES
?.historicalData ?? []) {
item.date = parseISO(item.date);
}
for (const item of data.fearAndGreedIndex.STOCKS?.historicalData ??
[]) {
item.date = parseISO(item.date);
}
return data;
})
);
}
public fetchSymbolItem({ public fetchSymbolItem({
dataSource, dataSource,
includeHistoricalData, includeHistoricalData,

6
libs/common/src/lib/config.ts

@ -4,8 +4,12 @@ import ms from 'ms';
export const ghostfolioPrefix = 'GF'; export const ghostfolioPrefix = 'GF';
export const ghostfolioScraperApiSymbolPrefix = `_${ghostfolioPrefix}_`; export const ghostfolioScraperApiSymbolPrefix = `_${ghostfolioPrefix}_`;
export const ghostfolioFearAndGreedIndexDataSource = DataSource.RAPID_API; export const ghostfolioFearAndGreedIndexDataSourceCryptocurrencies =
DataSource.MANUAL;
export const ghostfolioFearAndGreedIndexDataSourceStocks = DataSource.RAPID_API;
export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`; export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`;
export const ghostfolioFearAndGreedIndexSymbolCryptocurrencies = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_CRYPTOCURRENCIES`;
export const ghostfolioFearAndGreedIndexSymbolStocks = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_STOCKS`;
export const locale = 'en-US'; export const locale = 'en-US';

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,

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

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

1
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',
readMarketDataOfOwnAssetProfile: 'readMarketDataOfOwnAssetProfile', readMarketDataOfOwnAssetProfile: 'readMarketDataOfOwnAssetProfile',
readPlatforms: 'readPlatforms', readPlatforms: 'readPlatforms',
readTags: 'readTags', readTags: 'readTags',

5
libs/common/src/lib/routes/routes.ts

@ -94,6 +94,11 @@ export const internalRoutes: Record<string, InternalRoute> = {
routerLink: ['/home', 'markets'], routerLink: ['/home', 'markets'],
title: $localize`Markets` title: $localize`Markets`
}, },
marketsPremium: {
path: 'markets-premium',
routerLink: ['/home', 'markets-premium'],
title: $localize`Markets`
},
summary: { summary: {
path: 'summary', path: 'summary',
routerLink: ['/home', 'summary'], routerLink: ['/home', 'summary'],

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