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 2 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 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
### Changed

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

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

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

@ -29,6 +29,7 @@ import {
DEFAULT_DATE_RANGE,
DEFAULT_LANGUAGE_CODE,
DEFAULT_LOCALE,
PROPERTY_API_KEY_GHOSTFOLIO,
PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_REFERRAL_PARTNERS,
PROPERTY_SYSTEM_MESSAGE,
@ -546,6 +547,12 @@ export class UserService {
if (hasRole(user, Role.ADMIN)) {
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')) {

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

@ -27,7 +27,8 @@ import {
DataProviderHistoricalResponse,
DataProviderResponse,
LookupItem,
LookupResponse
LookupResponse,
MarketDataOfMarketsResponse
} from '@ghostfolio/common/interfaces';
import type { Granularity, UserWithSettings } from '@ghostfolio/common/types';
@ -194,11 +195,7 @@ export class DataProviderService implements OnModuleInit {
return DataSource[dataSource];
});
const ghostfolioApiKey = await this.propertyService.getByKey<string>(
PROPERTY_API_KEY_GHOSTFOLIO
);
if (ghostfolioApiKey) {
if (await this.isDataProviderGhostfolioConfigured()) {
dataSources.push('GHOSTFOLIO');
}
@ -551,6 +548,22 @@ export class DataProviderService implements OnModuleInit {
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({
items,
requestTimeout,
@ -807,6 +820,12 @@ export class DataProviderService implements OnModuleInit {
return response;
}
public async isDataProviderGhostfolioConfigured(): Promise<boolean> {
return !!(await this.propertyService.getByKey<string>(
PROPERTY_API_KEY_GHOSTFOLIO
));
}
public async search({
includeIndices = false,
query,

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

@ -5,6 +5,7 @@ import {
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetMarketDataOfMarketsParams,
GetQuotesParams,
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
@ -23,7 +24,9 @@ import {
DividendsResponse,
HistoricalResponse,
LookupResponse,
QuotesResponse
MarketDataOfMarketsResponse,
QuotesResponse,
SymbolItem
} from '@ghostfolio/common/interfaces';
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() {
return 20;
}

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

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

Loading…
Cancel
Save