From 70cd1a89b513bb58441d1b9f9c23a25cdf0b4816 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 10 Mar 2025 20:45:41 +0100 Subject: [PATCH] Feature/extend Ghostfolio data provider (#4420) * Extend Ghostfolio data provider --- .../ghostfolio/ghostfolio.controller.ts | 36 ++++++++++ .../ghostfolio/ghostfolio.service.ts | 43 +++++++++++- .../alpha-vantage/alpha-vantage.service.ts | 5 +- .../coingecko/coingecko.service.ts | 5 +- .../eod-historical-data.service.ts | 5 +- .../financial-modeling-prep.service.ts | 38 ++++------- .../ghostfolio/ghostfolio.service.ts | 68 +++++++++++++------ .../google-sheets/google-sheets.service.ts | 5 +- .../interfaces/data-provider.interface.ts | 10 ++- .../data-provider/manual/manual.service.ts | 5 +- .../rapid-api/rapid-api.service.ts | 5 +- .../yahoo-finance/yahoo-finance.service.ts | 5 +- libs/common/src/lib/interfaces/index.ts | 2 + ...tfolio-asset-profile-response.interface.ts | 4 ++ 14 files changed, 165 insertions(+), 71 deletions(-) create mode 100644 libs/common/src/lib/interfaces/responses/data-provider-ghostfolio-asset-profile-response.interface.ts diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts index f3386f8a7..83c7317f0 100644 --- a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts +++ b/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 */ diff --git a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts b/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts index 78685a61b..7281697bd 100644 --- a/apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts +++ b/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'); diff --git a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts index 5c9eee127..606e6b7fd 100644 --- a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts +++ b/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() diff --git a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts index fb1fa9b63..d53355b9c 100644 --- a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts +++ b/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, diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index e427a830a..376b8f159 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/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 { diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts index 6a3d0d41f..5216ed214 100644 --- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts +++ b/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`; } diff --git a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts index a674d479a..097464e2f 100644 --- a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts +++ b/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'; diff --git a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts index f18d670d1..0c466972d 100644 --- a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts +++ b/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() diff --git a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts index 5c316aac2..475205a01 100644 --- a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts +++ b/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; } diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts index dfda922c6..331806098 100644 --- a/apps/api/src/services/data-provider/manual/manual.service.ts +++ b/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() diff --git a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts index 4c9bb2717..7762be426 100644 --- a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts +++ b/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() diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts index 27da18ab0..72ae1ff97 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts +++ b/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); } diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 7ad4948dc..3dcbbb32a 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/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, diff --git a/libs/common/src/lib/interfaces/responses/data-provider-ghostfolio-asset-profile-response.interface.ts b/libs/common/src/lib/interfaces/responses/data-provider-ghostfolio-asset-profile-response.interface.ts new file mode 100644 index 000000000..7fd0314fb --- /dev/null +++ b/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> {}