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
This commit is contained in:
Thomas Kaul 2025-04-21 16:29:05 +02:00 committed by GitHub
parent 1ae5ba7f8a
commit d6e0b499d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 67 additions and 39 deletions

View File

@ -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

View File

@ -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({

View File

@ -405,12 +405,15 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}
public async search({ query }: GetSearchParams): Promise<LookupResponse> {
const assetProfileBySymbolMap: {
[symbol: string]: Partial<SymbolProfile>;
} = {};
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 {

View File

@ -27,9 +27,10 @@ import { map, Observable, Subject, takeUntil } from 'rxjs';
export class GfApiPageComponent implements OnInit {
public dividends$: Observable<DividendsResponse['dividends']>;
public historicalData$: Observable<HistoricalResponse['historicalData']>;
public isinLookupItems$: Observable<LookupResponse['items']>;
public lookupItems$: Observable<LookupResponse['items']>;
public quotes$: Observable<QuotesResponse['quotes']>;
public status$: Observable<DataProviderGhostfolioStatusResponse>;
public symbols$: Observable<LookupResponse['items']>;
private apiKey: string;
private unsubscribeSubject = new Subject<void>();
@ -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<QuotesResponse>('/api/v2/data-providers/ghostfolio/quotes', {
params,
headers: this.getHeaders()
})
.pipe(
map(({ quotes }) => {
return quotes;
}),
takeUntil(this.unsubscribeSubject)
);
}
private fetchStatus() {
return this.http
.get<DataProviderGhostfolioStatusResponse>(
'/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<QuotesResponse>('/api/v2/data-providers/ghostfolio/quotes', {
params,
headers: this.getHeaders()
})
.pipe(
map(({ quotes }) => {
return quotes;
}),
takeUntil(this.unsubscribeSubject)
);
}
private fetchStatus() {
return this.http
.get<DataProviderGhostfolioStatusResponse>(
'/api/v2/data-providers/ghostfolio/status',
{ headers: this.getHeaders() }
)
.pipe(takeUntil(this.unsubscribeSubject));
}
private getHeaders() {
return new HttpHeaders({
[HEADER_KEY_SKIP_INTERCEPTOR]: 'true',

View File

@ -3,10 +3,21 @@
<h2 class="text-center">Status</h2>
<div>{{ status$ | async | json }}</div>
</div>
<div class="mb-3">
<div>
<h2 class="text-center">Lookup</h2>
@if (symbols$) {
@let symbols = symbols$ | async;
@if (lookupItems$) {
@let symbols = lookupItems$ | async;
<ul>
@for (item of symbols; track item.symbol) {
<li>{{ item.name }} ({{ item.symbol }})</li>
}
</ul>
}
</div>
<div>
<h2 class="text-center">Lookup (ISIN)</h2>
@if (isinLookupItems$) {
@let symbols = isinLookupItems$ | async;
<ul>
@for (item of symbols; track item.symbol) {
<li>{{ item.name }} ({{ item.symbol }})</li>