From d6e0b499d9168815a7615268d5c9b8d5278f2767 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:29:05 +0200 Subject: [PATCH] Feature/migrate lookup by ISIN in Financial Modeling Prep service to stable API version (#4573) * Migrate lookup by ISIN to stable API version * Update changelog --- CHANGELOG.md | 1 + .../ghostfolio/ghostfolio.controller.ts | 5 +- .../financial-modeling-prep.service.ts | 25 +++++--- .../src/app/pages/api/api-page.component.ts | 58 ++++++++++--------- apps/client/src/app/pages/api/api-page.html | 17 +++++- 5 files changed, 67 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e836484a..038734f4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Made the historical market data editor expandable in the admin control panel - Parallelized the requests in the get quotes functionality of the _Financial Modeling Prep_ service +- Migrated the lookup functionality by `isin` of the _Financial Modeling Prep_ service to its stable API version ### Fixed 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 83c7317f0..83e1b5ced 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 @@ -24,6 +24,7 @@ import { } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; +import { isISIN } from 'class-validator'; import { getReasonPhrase, StatusCodes } from 'http-status-codes'; import { GetDividendsDto } from './get-dividends.dto'; @@ -301,7 +302,9 @@ export class GhostfolioController { try { const result = await this.ghostfolioService.lookup({ includeIndices, - query: query.toLowerCase() + query: isISIN(query.toUpperCase()) + ? query.toUpperCase() + : query.toLowerCase() }); await this.ghostfolioService.incrementDailyRequests({ 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 518056dfd..4e42201d0 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 @@ -405,12 +405,15 @@ export class FinancialModelingPrepService implements DataProviderInterface { } public async search({ query }: GetSearchParams): Promise { + const assetProfileBySymbolMap: { + [symbol: string]: Partial; + } = {}; let items: LookupItem[] = []; try { - if (isISIN(query)) { + if (isISIN(query?.toUpperCase())) { const result = await fetch( - `${this.getUrl({ version: 4 })}/search/isin?isin=${query}&apikey=${this.apiKey}`, + `${this.getUrl({ version: 'stable' })}/search-isin?isin=${query.toUpperCase()}&apikey=${this.apiKey}`, { signal: AbortSignal.timeout( this.configurationService.get('REQUEST_TIMEOUT') @@ -418,15 +421,23 @@ export class FinancialModelingPrepService implements DataProviderInterface { } ).then((res) => res.json()); - items = result.map(({ companyName, currency, symbol }) => { + await Promise.all( + result.map(({ symbol }) => { + return this.getAssetProfile({ symbol }).then((assetProfile) => { + assetProfileBySymbolMap[symbol] = assetProfile; + }); + }) + ); + + items = result.map(({ assetClass, assetSubClass, name, symbol }) => { return { - currency, + assetClass, + assetSubClass, symbol, - assetClass: undefined, // TODO - assetSubClass: undefined, // TODO + currency: assetProfileBySymbolMap[symbol]?.currency, dataProviderInfo: this.getDataProviderInfo(), dataSource: this.getName(), - name: this.formatName({ name: companyName }) + name: this.formatName({ name }) }; }); } else { diff --git a/apps/client/src/app/pages/api/api-page.component.ts b/apps/client/src/app/pages/api/api-page.component.ts index 350650060..7b385ec2f 100644 --- a/apps/client/src/app/pages/api/api-page.component.ts +++ b/apps/client/src/app/pages/api/api-page.component.ts @@ -27,9 +27,10 @@ import { map, Observable, Subject, takeUntil } from 'rxjs'; export class GfApiPageComponent implements OnInit { public dividends$: Observable; public historicalData$: Observable; + public isinLookupItems$: Observable; + public lookupItems$: Observable; public quotes$: Observable; public status$: Observable; - public symbols$: Observable; private apiKey: string; private unsubscribeSubject = new Subject(); @@ -41,9 +42,10 @@ export class GfApiPageComponent implements OnInit { this.dividends$ = this.fetchDividends({ symbol: 'KO' }); this.historicalData$ = this.fetchHistoricalData({ symbol: 'AAPL' }); + this.isinLookupItems$ = this.fetchLookupItems({ query: 'US0378331005' }); + this.lookupItems$ = this.fetchLookupItems({ query: 'apple' }); this.quotes$ = this.fetchQuotes({ symbols: ['AAPL', 'VOO.US'] }); this.status$ = this.fetchStatus(); - this.symbols$ = this.fetchSymbols({ query: 'apple' }); } public ngOnDestroy() { @@ -93,32 +95,7 @@ export class GfApiPageComponent implements OnInit { ); } - private fetchQuotes({ symbols }: { symbols: string[] }) { - const params = new HttpParams().set('symbols', symbols.join(',')); - - return this.http - .get('/api/v2/data-providers/ghostfolio/quotes', { - params, - headers: this.getHeaders() - }) - .pipe( - map(({ quotes }) => { - return quotes; - }), - takeUntil(this.unsubscribeSubject) - ); - } - - private fetchStatus() { - return this.http - .get( - '/api/v2/data-providers/ghostfolio/status', - { headers: this.getHeaders() } - ) - .pipe(takeUntil(this.unsubscribeSubject)); - } - - private fetchSymbols({ + private fetchLookupItems({ includeIndices = false, query }: { @@ -144,6 +121,31 @@ export class GfApiPageComponent implements OnInit { ); } + private fetchQuotes({ symbols }: { symbols: string[] }) { + const params = new HttpParams().set('symbols', symbols.join(',')); + + return this.http + .get('/api/v2/data-providers/ghostfolio/quotes', { + params, + headers: this.getHeaders() + }) + .pipe( + map(({ quotes }) => { + return quotes; + }), + takeUntil(this.unsubscribeSubject) + ); + } + + private fetchStatus() { + return this.http + .get( + '/api/v2/data-providers/ghostfolio/status', + { headers: this.getHeaders() } + ) + .pipe(takeUntil(this.unsubscribeSubject)); + } + private getHeaders() { return new HttpHeaders({ [HEADER_KEY_SKIP_INTERCEPTOR]: 'true', diff --git a/apps/client/src/app/pages/api/api-page.html b/apps/client/src/app/pages/api/api-page.html index a1f286c07..d8bfc75d7 100644 --- a/apps/client/src/app/pages/api/api-page.html +++ b/apps/client/src/app/pages/api/api-page.html @@ -3,10 +3,21 @@

Status

{{ status$ | async | json }}
-
+

Lookup

- @if (symbols$) { - @let symbols = symbols$ | async; + @if (lookupItems$) { + @let symbols = lookupItems$ | async; +
    + @for (item of symbols; track item.symbol) { +
  • {{ item.name }} ({{ item.symbol }})
  • + } +
+ } +
+
+

Lookup (ISIN)

+ @if (isinLookupItems$) { + @let symbols = isinLookupItems$ | async;
    @for (item of symbols; track item.symbol) {
  • {{ item.name }} ({{ item.symbol }})