Browse Source

Feature/extend Ghostfolio data provider (#4420)

* Extend Ghostfolio data provider
pull/4422/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
70cd1a89b5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 36
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
  2. 43
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  3. 5
      apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
  4. 5
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  5. 5
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  6. 38
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  7. 68
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  8. 5
      apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
  9. 10
      apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
  10. 5
      apps/api/src/services/data-provider/manual/manual.service.ts
  11. 5
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  12. 5
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  13. 2
      libs/common/src/lib/interfaces/index.ts
  14. 4
      libs/common/src/lib/interfaces/responses/data-provider-ghostfolio-asset-profile-response.interface.ts

36
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts

@ -2,6 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { parseDate } from '@ghostfolio/common/helper';
import {
DataProviderGhostfolioAssetProfileResponse,
DataProviderGhostfolioStatusResponse,
DividendsResponse,
HistoricalResponse,
@ -37,6 +38,41 @@ export class GhostfolioController {
@Inject(REQUEST) private readonly request: RequestWithUser
) {}
@Get('asset-profile/:symbol')
@HasPermission(permissions.enableDataProviderGhostfolio)
@UseGuards(AuthGuard('api-key'), HasPermissionGuard)
public async getAssetProfile(
@Param('symbol') symbol: string
): Promise<DataProviderGhostfolioAssetProfileResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests > maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try {
const assetProfile = await this.ghostfolioService.getAssetProfile({
symbol
});
await this.ghostfolioService.incrementDailyRequests({
userId: this.request.user.id
});
return assetProfile;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
StatusCodes.INTERNAL_SERVER_ERROR
);
}
}
/**
* @deprecated
*/

43
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -1,6 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import {
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetQuotesParams,
@ -15,6 +16,7 @@ import {
} from '@ghostfolio/common/config';
import { PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS } from '@ghostfolio/common/config';
import {
DataProviderGhostfolioAssetProfileResponse,
DataProviderInfo,
DividendsResponse,
HistoricalResponse,
@ -25,7 +27,7 @@ import {
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client';
import { DataSource, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js';
@Injectable()
@ -37,6 +39,44 @@ export class GhostfolioService {
private readonly propertyService: PropertyService
) {}
public async getAssetProfile({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbol
}: GetAssetProfileParams) {
let result: DataProviderGhostfolioAssetProfileResponse = {};
try {
const promises: Promise<Partial<SymbolProfile>>[] = [];
for (const dataProviderService of this.getDataProviderServices()) {
promises.push(
dataProviderService
.getAssetProfile({
requestTimeout,
symbol
})
.then((assetProfile) => {
result = {
...result,
...assetProfile,
dataSource: DataSource.GHOSTFOLIO
};
return assetProfile;
})
);
}
await Promise.all(promises);
return result;
} catch (error) {
Logger.error(error, 'GhostfolioService');
throw error;
}
}
public async getDividends({
from,
granularity,
@ -277,6 +317,7 @@ export class GhostfolioService {
});
results.items = filteredItems;
return results;
} catch (error) {
Logger.error(error, 'GhostfolioService');

5
apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts

@ -1,6 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetQuotesParams,
@ -41,9 +42,7 @@ export class AlphaVantageService implements DataProviderInterface {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
return {
symbol,
dataSource: this.getName()

5
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -1,6 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetQuotesParams,
@ -56,9 +57,7 @@ export class CoinGeckoService implements DataProviderInterface {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
const response: Partial<SymbolProfile> = {
symbol,
assetClass: AssetClass.LIQUIDITY,

5
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -1,6 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetQuotesParams,
@ -51,9 +52,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
const [searchResult] = await this.getSearchResult(symbol);
return {

38
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -2,6 +2,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import {
DataProviderInterface,
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetQuotesParams,
@ -56,10 +57,9 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
public async getAssetProfile({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
const response: Partial<SymbolProfile> = {
symbol,
dataSource: this.getName()
@ -70,9 +70,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
const [quote] = await fetch(
`${this.URL}/quote/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
@ -84,9 +82,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
const [assetProfile] = await fetch(
`${this.URL}/profile/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
@ -100,9 +96,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
const etfCountryWeightings = await fetch(
`${this.URL}/etf-country-weightings/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
@ -127,9 +121,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
const [etfInformation] = await fetch(
`${this.getUrl({ version: 4 })}/etf-info?symbol=${symbol}&apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
@ -140,9 +132,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
const [portfolioDate] = await fetch(
`${this.getUrl({ version: 4 })}/etf-holdings/portfolio-date?symbol=${symbol}&apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
@ -150,9 +140,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
const etfHoldings = await fetch(
`${this.getUrl({ version: 4 })}/etf-holdings?date=${portfolioDate.date}&symbol=${symbol}&apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
@ -170,9 +158,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
const etfSectorWeightings = await fetch(
`${this.URL}/etf-sector-weightings/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
@ -211,7 +197,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
if (error?.name === 'AbortError') {
message = `RequestError: The operation to get the asset profile for ${symbol} was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000
requestTimeout / 1000
).toFixed(3)} seconds`;
}
@ -376,7 +362,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
if (error?.name === 'AbortError') {
message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000
requestTimeout / 1000
).toFixed(3)} seconds`;
}

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

@ -2,6 +2,7 @@ import { environment } from '@ghostfolio/api/environments/environment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetQuotesParams,
@ -18,6 +19,7 @@ import {
} from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
DataProviderGhostfolioAssetProfileResponse,
DataProviderInfo,
DividendsResponse,
HistoricalResponse,
@ -46,21 +48,46 @@ export class GhostfolioService implements DataProviderInterface {
}
public async getAssetProfile({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
const { items } = await this.search({ query: symbol });
const searchResult = items?.[0];
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
let response: DataProviderGhostfolioAssetProfileResponse = {};
return {
symbol,
assetClass: searchResult?.assetClass,
assetSubClass: searchResult?.assetSubClass,
currency: searchResult?.currency,
dataSource: this.getName(),
name: searchResult?.name
};
try {
const assetProfile = (await fetch(
`${this.URL}/v1/data-providers/ghostfolio/asset-profile/${symbol}`,
{
headers: await this.getRequestHeaders(),
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) =>
res.json()
)) as DataProviderGhostfolioAssetProfileResponse;
response = assetProfile;
} catch (error) {
let message = error;
if (error.name === 'AbortError') {
message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${(
requestTimeout / 1000
).toFixed(3)} seconds`;
} else if (error.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS) {
message = 'RequestError: The daily request limit has been exceeded';
} else if (error.response?.statusCode === StatusCodes.UNAUTHORIZED) {
if (!error.request?.options?.headers?.authorization?.includes('-')) {
message =
'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.';
} else {
message =
'RequestError: The provided API key has expired. Please request a new one and update it in the Settings section of the Admin Control panel.';
}
}
Logger.error(message, 'GhostfolioService');
}
return response;
}
public getDataProviderInfo(): DataProviderInfo {
@ -203,7 +230,7 @@ export class GhostfolioService implements DataProviderInterface {
if (error.name === 'AbortError') {
message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000
requestTimeout / 1000
).toFixed(3)} seconds`;
} else if (error.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS) {
message = 'RequestError: The daily request limit has been exceeded';
@ -224,10 +251,13 @@ export class GhostfolioService implements DataProviderInterface {
}
public getTestSymbol() {
return 'AAPL.US';
return 'AAPL';
}
public async search({ query }: GetSearchParams): Promise<LookupResponse> {
public async search({
requestTimeout = this.configurationService.get('REQUEST_TIMEOUT'),
query
}: GetSearchParams): Promise<LookupResponse> {
let searchResult: LookupResponse = { items: [] };
try {
@ -235,9 +265,7 @@ export class GhostfolioService implements DataProviderInterface {
`${this.URL}/v2/data-providers/ghostfolio/lookup?query=${query}`,
{
headers: await this.getRequestHeaders(),
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json())) as LookupResponse;
} catch (error) {
@ -245,7 +273,7 @@ export class GhostfolioService implements DataProviderInterface {
if (error.name === 'AbortError') {
message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000
requestTimeout / 1000
).toFixed(3)} seconds`;
} else if (error.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS) {
message = 'RequestError: The daily request limit has been exceeded';

5
apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts

@ -1,6 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetQuotesParams,
@ -37,9 +38,7 @@ export class GoogleSheetsService implements DataProviderInterface {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
return {
symbol,
dataSource: this.getName()

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

@ -15,9 +15,7 @@ export interface DataProviderInterface {
getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>>;
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>>;
getDataProviderInfo(): DataProviderInfo;
@ -55,6 +53,11 @@ export interface DataProviderInterface {
search({ includeIndices, query }: GetSearchParams): Promise<LookupResponse>;
}
export interface GetAssetProfileParams {
requestTimeout?: number;
symbol: string;
}
export interface GetDividendsParams {
from: Date;
granularity?: Granularity;
@ -79,5 +82,6 @@ export interface GetQuotesParams {
export interface GetSearchParams {
includeIndices?: boolean;
query: string;
requestTimeout?: number;
userId?: string;
}

5
apps/api/src/services/data-provider/manual/manual.service.ts

@ -1,6 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetQuotesParams,
@ -43,9 +44,7 @@ export class ManualService implements DataProviderInterface {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
const assetProfile: Partial<SymbolProfile> = {
symbol,
dataSource: this.getName()

5
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -1,6 +1,7 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import {
DataProviderInterface,
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetQuotesParams,
@ -33,9 +34,7 @@ export class RapidApiService implements DataProviderInterface {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
return {
symbol,
dataSource: this.getName()

5
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -2,6 +2,7 @@ import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/c
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
import {
DataProviderInterface,
GetAssetProfileParams,
GetDividendsParams,
GetHistoricalParams,
GetQuotesParams,
@ -43,9 +44,7 @@ export class YahooFinanceService implements DataProviderInterface {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
}: GetAssetProfileParams): Promise<Partial<SymbolProfile>> {
return this.yahooFinanceDataEnhancerService.getAssetProfile(symbol);
}

2
libs/common/src/lib/interfaces/index.ts

@ -41,6 +41,7 @@ import type { AccountBalancesResponse } from './responses/account-balances-respo
import type { AiPromptResponse } from './responses/ai-prompt-response.interface';
import type { ApiKeyResponse } from './responses/api-key-response.interface';
import type { BenchmarkResponse } from './responses/benchmark-response.interface';
import type { DataProviderGhostfolioAssetProfileResponse } from './responses/data-provider-ghostfolio-asset-profile-response.interface';
import type { DataProviderGhostfolioStatusResponse } from './responses/data-provider-ghostfolio-status-response.interface';
import type { DividendsResponse } from './responses/dividends-response.interface';
import type { ResponseError } from './responses/errors.interface';
@ -83,6 +84,7 @@ export {
BenchmarkProperty,
BenchmarkResponse,
Coupon,
DataProviderGhostfolioAssetProfileResponse,
DataProviderGhostfolioStatusResponse,
DataProviderInfo,
DividendsResponse,

4
libs/common/src/lib/interfaces/responses/data-provider-ghostfolio-asset-profile-response.interface.ts

@ -0,0 +1,4 @@
import { SymbolProfile } from '@prisma/client';
export interface DataProviderGhostfolioAssetProfileResponse
extends Partial<SymbolProfile> {}
Loading…
Cancel
Save