You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

416 lines
12 KiB

import { environment } from '@ghostfolio/api/environments/environment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetMarketDataOfMarketsParams,
GetQuotesParams,
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
HEADER_KEY_TOKEN,
PROPERTY_API_KEY_GHOSTFOLIO
} from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
DataProviderGhostfolioAssetProfileResponse,
DataProviderHistoricalResponse,
DataProviderInfo,
DataProviderResponse,
DividendsResponse,
HistoricalResponse,
LookupResponse,
MarketDataOfMarketsResponse,
QuotesResponse,
SymbolItem
} from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import { format } from 'date-fns';
import { StatusCodes } from 'http-status-codes';
@Injectable()
export class GhostfolioService implements DataProviderInterface {
private readonly logger = new Logger(GhostfolioService.name);
private readonly URL = environment.production
? 'https://ghostfol.io/api'
: `${this.configurationService.get('ROOT_URL')}/api`;
public constructor(
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService,
private readonly propertyService: PropertyService
) {}
public canHandle() {
return true;
}
public async getAssetProfile({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbol
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
let assetProfile: DataProviderGhostfolioAssetProfileResponse;
try {
const response = await this.fetchService.fetch(
`${this.URL}/v1/data-providers/ghostfolio/asset-profile/${symbol}`,
{
headers: await this.getRequestHeaders(),
signal: AbortSignal.timeout(requestTimeout)
}
);
if (!response.ok) {
throw new Response(await response.text(), {
status: response.status,
statusText: response.statusText
});
}
assetProfile =
(await response.json()) as DataProviderGhostfolioAssetProfileResponse;
} catch (error) {
let message = error;
if (['AbortError', 'TimeoutError'].includes(error?.name)) {
message = `RequestError: The operation to get the asset profile for ${symbol} 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 assetProfile;
}
public getDataProviderInfo(): DataProviderInfo {
return {
dataSource: DataSource.GHOSTFOLIO,
isPremium: true,
name: 'Ghostfolio',
url: 'https://ghostfol.io'
};
}
public async getDividends({
from,
granularity = 'day',
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbol,
to
}: GetDividendsParams): Promise<{
[date: string]: DataProviderHistoricalResponse;
}> {
let dividends: {
[date: string]: DataProviderHistoricalResponse;
} = {};
try {
const queryParams = new URLSearchParams({
granularity,
from: format(from, DATE_FORMAT),
to: format(to, DATE_FORMAT)
});
const response = await this.fetchService.fetch(
`${this.URL}/v2/data-providers/ghostfolio/dividends/${symbol}?${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
});
}
dividends = ((await response.json()) as DividendsResponse).dividends;
} catch (error) {
let message = error;
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 dividends;
}
public async getHistorical({
from,
granularity = 'day',
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbol,
to
}: GetHistoricalParams): Promise<{
[date: string]: DataProviderHistoricalResponse;
}> {
try {
const queryParams = new URLSearchParams({
granularity,
from: format(from, DATE_FORMAT),
to: format(to, DATE_FORMAT)
});
const response = await this.fetchService.fetch(
`${this.URL}/v2/data-providers/ghostfolio/historical/${symbol}?${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
});
}
const { historicalData } = (await response.json()) as HistoricalResponse;
return historicalData;
} catch (error) {
if (error?.status === StatusCodes.TOO_MANY_REQUESTS) {
error.name = 'RequestError';
error.message =
'RequestError: The daily request limit has been exceeded';
} else if (
[StatusCodes.FORBIDDEN, StatusCodes.UNAUTHORIZED].includes(
error?.status
)
) {
error.name = 'RequestError';
error.message =
'RequestError: The API key is invalid. Please update it in the Settings section of the Admin Control panel.';
}
this.logger.error(error.message);
throw new Error(
`Could not get historical market data for ${symbol} (${this.getName()}) from ${format(
from,
DATE_FORMAT
)} to ${format(to, DATE_FORMAT)}: [${error.name}] ${error.message}`
);
}
}
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;
}
public getName(): DataSource {
return DataSource.GHOSTFOLIO;
}
public async getQuotes({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbols
}: GetQuotesParams): Promise<{
[symbol: string]: DataProviderResponse;
}> {
let quotes: { [symbol: string]: DataProviderResponse } = {};
if (symbols.length <= 0) {
return quotes;
}
try {
const queryParams = new URLSearchParams({
symbols: symbols.join(',')
});
const response = await this.fetchService.fetch(
`${this.URL}/v2/data-providers/ghostfolio/quotes?${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
});
}
quotes = ((await response.json()) as QuotesResponse).quotes;
} catch (error) {
let message = error;
if (['AbortError', 'TimeoutError'].includes(error?.name)) {
message = `RequestError: The operation to get the quotes for ${symbols.join(
', '
)} 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 quotes;
}
public getTestSymbol() {
return 'AAPL';
}
public async search({
query,
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT')
}: GetSearchParams): Promise<LookupResponse> {
let searchResult: LookupResponse = { items: [] };
try {
const queryParams = new URLSearchParams({
query
});
const response = await this.fetchService.fetch(
`${this.URL}/v2/data-providers/ghostfolio/lookup?${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
});
}
searchResult = (await response.json()) as LookupResponse;
} catch (error) {
let message = error;
if (['AbortError', 'TimeoutError'].includes(error?.name)) {
message = `RequestError: The operation to search for ${query} 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 searchResult;
}
private async getRequestHeaders() {
const apiKey = await this.propertyService.getByKey<string>(
PROPERTY_API_KEY_GHOSTFOLIO
);
return {
[HEADER_KEY_TOKEN]: `Api-Key ${apiKey}`
};
}
}