Browse Source

Feature/support Fear & Greed index via Ghostfolio data provider in self-hosted environments (#7364)

* Support Fear & Greed index via Ghostfolio data provider in self-hosted environments

* Update changelog
pull/7366/head
Thomas Kaul 3 days ago
committed by GitHub
parent
commit
ae257f8acb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 6
      apps/api/src/app/symbol/symbol.service.ts
  3. 7
      apps/api/src/app/user/user.service.ts
  4. 31
      apps/api/src/services/data-provider/data-provider.service.ts
  5. 62
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  6. 13
      apps/api/src/services/data-provider/interfaces/data-provider.interface.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added support for the _Fear & Greed Index_ (market mood) via the `GHOSTFOLIO` data provider in self-hosted environments
- Added a _Storybook_ story for the copy-to-clipboard functionality in the value component - Added a _Storybook_ story for the copy-to-clipboard functionality in the value component
### Changed ### Changed

6
apps/api/src/app/symbol/symbol.service.ts

@ -133,6 +133,12 @@ export class SymbolService {
}: { }: {
includeHistoricalData: number; includeHistoricalData: number;
}): Promise<MarketDataOfMarketsResponse> { }): Promise<MarketDataOfMarketsResponse> {
if (await this.dataProviderService.isDataProviderGhostfolioConfigured()) {
return this.dataProviderService.getMarketDataOfMarkets({
includeHistoricalData
});
}
const [ const [
marketDataFearAndGreedIndexCryptocurrencies, marketDataFearAndGreedIndexCryptocurrencies,
marketDataFearAndGreedIndexStocks marketDataFearAndGreedIndexStocks

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

@ -29,6 +29,7 @@ import {
DEFAULT_DATE_RANGE, DEFAULT_DATE_RANGE,
DEFAULT_LANGUAGE_CODE, DEFAULT_LANGUAGE_CODE,
DEFAULT_LOCALE, DEFAULT_LOCALE,
PROPERTY_API_KEY_GHOSTFOLIO,
PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_REFERRAL_PARTNERS, PROPERTY_REFERRAL_PARTNERS,
PROPERTY_SYSTEM_MESSAGE, PROPERTY_SYSTEM_MESSAGE,
@ -546,6 +547,12 @@ export class UserService {
if (hasRole(user, Role.ADMIN)) { if (hasRole(user, Role.ADMIN)) {
currentPermissions.push(permissions.syncDemoUserAccount); currentPermissions.push(permissions.syncDemoUserAccount);
} }
} else {
if (
await this.propertyService.getByKey<string>(PROPERTY_API_KEY_GHOSTFOLIO)
) {
currentPermissions.push(permissions.readMarketDataOfMarkets);
}
} }
if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) { if (this.configurationService.get('ENABLE_FEATURE_READ_ONLY_MODE')) {

31
apps/api/src/services/data-provider/data-provider.service.ts

@ -27,7 +27,8 @@ import {
DataProviderHistoricalResponse, DataProviderHistoricalResponse,
DataProviderResponse, DataProviderResponse,
LookupItem, LookupItem,
LookupResponse LookupResponse,
MarketDataOfMarketsResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import type { Granularity, UserWithSettings } from '@ghostfolio/common/types'; import type { Granularity, UserWithSettings } from '@ghostfolio/common/types';
@ -194,11 +195,7 @@ export class DataProviderService implements OnModuleInit {
return DataSource[dataSource]; return DataSource[dataSource];
}); });
const ghostfolioApiKey = await this.propertyService.getByKey<string>( if (await this.isDataProviderGhostfolioConfigured()) {
PROPERTY_API_KEY_GHOSTFOLIO
);
if (ghostfolioApiKey) {
dataSources.push('GHOSTFOLIO'); dataSources.push('GHOSTFOLIO');
} }
@ -551,6 +548,22 @@ export class DataProviderService implements OnModuleInit {
return result; return result;
} }
public async getMarketDataOfMarkets({
includeHistoricalData
}: {
includeHistoricalData: number;
}): Promise<MarketDataOfMarketsResponse> {
const dataProvider = this.getDataProvider(DataSource.GHOSTFOLIO);
if (!dataProvider.getMarketDataOfMarkets) {
throw new Error(
`The data provider (${DataSource.GHOSTFOLIO}) does not support the market data of markets`
);
}
return dataProvider.getMarketDataOfMarkets({ includeHistoricalData });
}
public async getQuotes({ public async getQuotes({
items, items,
requestTimeout, requestTimeout,
@ -807,6 +820,12 @@ export class DataProviderService implements OnModuleInit {
return response; return response;
} }
public async isDataProviderGhostfolioConfigured(): Promise<boolean> {
return !!(await this.propertyService.getByKey<string>(
PROPERTY_API_KEY_GHOSTFOLIO
));
}
public async search({ public async search({
includeIndices = false, includeIndices = false,
query, query,

62
apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts

@ -5,6 +5,7 @@ import {
GetAssetProfileParams, GetAssetProfileParams,
GetDividendsParams, GetDividendsParams,
GetHistoricalParams, GetHistoricalParams,
GetMarketDataOfMarketsParams,
GetQuotesParams, GetQuotesParams,
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
@ -23,7 +24,9 @@ import {
DividendsResponse, DividendsResponse,
HistoricalResponse, HistoricalResponse,
LookupResponse, LookupResponse,
QuotesResponse MarketDataOfMarketsResponse,
QuotesResponse,
SymbolItem
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
@ -224,6 +227,63 @@ export class GhostfolioService implements DataProviderInterface {
} }
} }
public async getMarketDataOfMarkets({
includeHistoricalData = 0,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetMarketDataOfMarketsParams): Promise<MarketDataOfMarketsResponse> {
let marketDataOfMarkets: MarketDataOfMarketsResponse = {
fearAndGreedIndex: {
CRYPTOCURRENCIES: {} as SymbolItem,
STOCKS: {} as SymbolItem
}
};
try {
const queryParams = new URLSearchParams({
includeHistoricalData: includeHistoricalData.toString()
});
const response = await this.fetchService.fetch(
`${this.URL}/v1/data-providers/ghostfolio/markets?${queryParams.toString()}`,
{
headers: await this.getRequestHeaders(),
signal: AbortSignal.timeout(requestTimeout)
}
);
if (!response.ok) {
throw new Response(await response.text(), {
status: response.status,
statusText: response.statusText
});
}
marketDataOfMarkets =
(await response.json()) as MarketDataOfMarketsResponse;
} catch (error) {
let message = error;
if (['AbortError', 'TimeoutError'].includes(error?.name)) {
message = `RequestError: The operation to get the market data of markets was aborted because the request to the data provider took more than ${(
requestTimeout / 1000
).toFixed(3)} seconds`;
} else if (error?.status === StatusCodes.TOO_MANY_REQUESTS) {
message = 'RequestError: The daily request limit has been exceeded';
} else if (
[StatusCodes.FORBIDDEN, StatusCodes.UNAUTHORIZED].includes(
error?.status
)
) {
message =
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
}
this.logger.error(message);
}
return marketDataOfMarkets;
}
public getMaxNumberOfSymbolsPerRequest() { public getMaxNumberOfSymbolsPerRequest() {
return 20; return 20;
} }

13
apps/api/src/services/data-provider/interfaces/data-provider.interface.ts

@ -2,7 +2,8 @@ import {
DataProviderHistoricalResponse, DataProviderHistoricalResponse,
DataProviderInfo, DataProviderInfo,
DataProviderResponse, DataProviderResponse,
LookupResponse LookupResponse,
MarketDataOfMarketsResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
@ -37,6 +38,11 @@ export interface DataProviderInterface {
[symbol: string]: { [date: string]: DataProviderHistoricalResponse }; [symbol: string]: { [date: string]: DataProviderHistoricalResponse };
}>; // TODO: Return only one symbol }>; // TODO: Return only one symbol
getMarketDataOfMarkets?({
includeHistoricalData,
requestTimeout
}: GetMarketDataOfMarketsParams): Promise<MarketDataOfMarketsResponse>;
getMaxNumberOfSymbolsPerRequest?(): number; getMaxNumberOfSymbolsPerRequest?(): number;
getName(): DataSource; getName(): DataSource;
@ -72,6 +78,11 @@ export interface GetHistoricalParams {
to: Date; to: Date;
} }
export interface GetMarketDataOfMarketsParams {
includeHistoricalData?: number;
requestTimeout?: number;
}
export interface GetQuotesParams { export interface GetQuotesParams {
requestTimeout?: number; requestTimeout?: number;
symbols: string[]; symbols: string[];

Loading…
Cancel
Save