From 6ef8598d726113b21df1ab43f889b7be1c05f72a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 23 Nov 2024 19:11:16 +0100 Subject: [PATCH] Set up Ghostfolio data provider --- apps/api/src/app/import/import.service.ts | 3 +- .../data-provider/data-provider.module.ts | 5 + .../data-provider/data-provider.service.ts | 29 +++- .../ghostfolio/ghostfolio.service.ts | 132 ++++++++++++++++++ libs/common/src/lib/config.ts | 1 + 5 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 30415970d..e51696b56 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -582,12 +582,13 @@ export class ImportService { const assetProfiles: { [assetProfileIdentifier: string]: Partial; } = {}; + const dataSources = await this.dataProviderService.getDataSources(); for (const [ index, { currency, dataSource, symbol, type } ] of activitiesDto.entries()) { - if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) { + if (!dataSources.includes(dataSource)) { throw new Error( `activities.${index}.dataSource ("${dataSource}") is not valid` ); diff --git a/apps/api/src/services/data-provider/data-provider.module.ts b/apps/api/src/services/data-provider/data-provider.module.ts index dcfc756f2..71b54f01e 100644 --- a/apps/api/src/services/data-provider/data-provider.module.ts +++ b/apps/api/src/services/data-provider/data-provider.module.ts @@ -5,6 +5,7 @@ import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alph import { CoinGeckoService } from '@ghostfolio/api/services/data-provider/coingecko/coingecko.service'; import { EodHistoricalDataService } from '@ghostfolio/api/services/data-provider/eod-historical-data/eod-historical-data.service'; import { FinancialModelingPrepService } from '@ghostfolio/api/services/data-provider/financial-modeling-prep/financial-modeling-prep.service'; +import { GhostfolioService } from '@ghostfolio/api/services/data-provider/ghostfolio/ghostfolio.service'; import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service'; import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service'; @@ -37,6 +38,7 @@ import { DataProviderService } from './data-provider.service'; DataProviderService, EodHistoricalDataService, FinancialModelingPrepService, + GhostfolioService, GoogleSheetsService, ManualService, RapidApiService, @@ -47,6 +49,7 @@ import { DataProviderService } from './data-provider.service'; CoinGeckoService, EodHistoricalDataService, FinancialModelingPrepService, + GhostfolioService, GoogleSheetsService, ManualService, RapidApiService, @@ -58,6 +61,7 @@ import { DataProviderService } from './data-provider.service'; coinGeckoService, eodHistoricalDataService, financialModelingPrepService, + ghostfolioService, googleSheetsService, manualService, rapidApiService, @@ -67,6 +71,7 @@ import { DataProviderService } from './data-provider.service'; coinGeckoService, eodHistoricalDataService, financialModelingPrepService, + ghostfolioService, googleSheetsService, manualService, rapidApiService, diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index c8a7422d0..50d14be89 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -11,6 +11,7 @@ import { PropertyService } from '@ghostfolio/api/services/property/property.serv import { DEFAULT_CURRENCY, DERIVED_CURRENCIES, + PROPERTY_API_KEY_GHOSTFOLIO, PROPERTY_DATA_SOURCE_MAPPING } from '@ghostfolio/common/config'; import { @@ -153,6 +154,24 @@ export class DataProviderService { return DataSource[this.configurationService.get('DATA_SOURCE_IMPORT')]; } + public async getDataSources(): Promise { + const dataSources: DataSource[] = this.configurationService + .get('DATA_SOURCES') + .map((dataSource) => { + return DataSource[dataSource]; + }); + + const ghostfolioApiKey = (await this.propertyService.getByKey( + PROPERTY_API_KEY_GHOSTFOLIO + )) as string; + + if (ghostfolioApiKey) { + dataSources.push('GHOSTFOLIO'); + } + + return dataSources.sort(); + } + public async getDividends({ dataSource, from, @@ -589,11 +608,11 @@ export class DataProviderService { return { items: lookupItems }; } - const dataProviderServices = this.configurationService - .get('DATA_SOURCES') - .map((dataSource) => { - return this.getDataProvider(DataSource[dataSource]); - }); + const dataSources = await this.getDataSources(); + + const dataProviderServices = dataSources.map((dataSource) => { + return this.getDataProvider(DataSource[dataSource]); + }); for (const dataProviderService of dataProviderServices) { promises.push( diff --git a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts new file mode 100644 index 000000000..c07f3db61 --- /dev/null +++ b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts @@ -0,0 +1,132 @@ +import { environment } from '@ghostfolio/api/environments/environment'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { + DataProviderInterface, + GetDividendsParams, + GetHistoricalParams, + GetQuotesParams, + GetSearchParams +} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse +} from '@ghostfolio/api/services/interfaces/interfaces'; +import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { PROPERTY_API_KEY_GHOSTFOLIO } from '@ghostfolio/common/config'; +import { + DataProviderInfo, + LookupResponse +} from '@ghostfolio/common/interfaces'; + +import { Injectable, Logger } from '@nestjs/common'; +import { DataSource, SymbolProfile } from '@prisma/client'; +import got from 'got'; + +@Injectable() +export class GhostfolioService implements DataProviderInterface { + private apiKey: string; + private readonly URL = environment.production + ? 'https://ghostfol.io/api' + : `${this.configurationService.get('ROOT_URL')}/api`; + + public constructor( + private readonly configurationService: ConfigurationService, + private readonly propertyService: PropertyService + ) { + void this.initialize(); + } + + public async initialize() { + this.apiKey = (await this.propertyService.getByKey( + PROPERTY_API_KEY_GHOSTFOLIO + )) as string; + } + + public canHandle() { + return true; + } + + public async getAssetProfile({ + symbol + }: { + symbol: string; + }): Promise> { + return { + symbol, + dataSource: this.getName() + }; + } + + public getDataProviderInfo(): DataProviderInfo { + return { + isPremium: true, + name: 'Ghostfolio', + url: 'https://ghostfo.io' + }; + } + + public async getDividends({}: GetDividendsParams) { + return {}; + } + + public async getHistorical({}: GetHistoricalParams): Promise<{ + [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; + }> { + // TODO + return {}; + } + + public getMaxNumberOfSymbolsPerRequest() { + return 20; + } + + public getName(): DataSource { + return DataSource.GHOSTFOLIO; + } + + public async getQuotes({}: GetQuotesParams): Promise<{ + [symbol: string]: IDataProviderResponse; + }> { + // TODO + return {}; + } + + public getTestSymbol() { + return 'AAPL.US'; + } + + public async search({ query }: GetSearchParams): Promise { + let searchResult: LookupResponse = { items: [] }; + + try { + const abortController = new AbortController(); + + setTimeout(() => { + abortController.abort(); + }, this.configurationService.get('REQUEST_TIMEOUT')); + + searchResult = await got( + `${this.URL}/v1/data-providers/ghostfolio/lookup?query=${query}`, + { + headers: { + Authorization: `Bearer ${this.apiKey}` + }, + // @ts-ignore + signal: abortController.signal + } + ).json(); + } catch (error) { + let message = error; + + if (error?.code === 'ABORT_ERR') { + 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' + )}ms`; + } + + Logger.error(message, 'GhostfolioService'); + } + + return searchResult; + } +} diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 929ba9f01..91c38b12c 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -111,6 +111,7 @@ export const MAX_TOP_HOLDINGS = 50; export const NUMERICAL_PRECISION_THRESHOLD = 100000; +export const PROPERTY_API_KEY_GHOSTFOLIO = 'API_KEY_GHOSTFOLIO'; export const PROPERTY_BENCHMARKS = 'BENCHMARKS'; export const PROPERTY_BETTER_UPTIME_MONITOR_ID = 'BETTER_UPTIME_MONITOR_ID'; export const PROPERTY_COUNTRIES_OF_SUBSCRIBERS = 'COUNTRIES_OF_SUBSCRIBERS';