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:
parent
1ae5ba7f8a
commit
d6e0b499d9
@ -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
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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 {
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user