Browse Source

Support Fear & Greed Index from GHOSTFOLIO data provider for self-hosted environments

pull/7364/head
Thomas Kaul 4 days ago
parent
commit
bee407aa3b
  1. 2
      apps/api/src/app/symbol/symbol.module.ts
  2. 18
      apps/api/src/app/symbol/symbol.service.ts
  3. 7
      apps/api/src/app/user/user.service.ts
  4. 19
      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

2
apps/api/src/app/symbol/symbol.module.ts

@ -3,6 +3,7 @@ import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptor
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { Module } from '@nestjs/common';
@ -16,6 +17,7 @@ import { SymbolService } from './symbol.service';
DataProviderModule,
MarketDataModule,
PrismaModule,
PropertyModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
],

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

@ -1,10 +1,12 @@
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { DataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks
ghostfolioFearAndGreedIndexSymbolStocks,
PROPERTY_API_KEY_GHOSTFOLIO
} from '@ghostfolio/common/config';
import {
DATE_FORMAT,
@ -28,7 +30,8 @@ export class SymbolService {
public constructor(
private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService
private readonly marketDataService: MarketDataService,
private readonly propertyService: PropertyService
) {}
public async get({
@ -133,6 +136,17 @@ export class SymbolService {
}: {
includeHistoricalData: number;
}): Promise<MarketDataOfMarketsResponse> {
const hasApiKeyForDataProviderGhostfolio =
!!(await this.propertyService.getByKey<string>(
PROPERTY_API_KEY_GHOSTFOLIO
));
if (hasApiKeyForDataProviderGhostfolio) {
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')) {

19
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';
@ -551,6 +552,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,

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