Browse Source

Feature/extend asset profile data in financial modeling prep service (#4206)

* Extend asset profile data

* Update changelog
pull/1946/merge
Thomas Kaul 2 weeks ago
committed by GitHub
parent
commit
511a2d6d0d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 182
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Extended the asset profile data in the _Financial Modeling Prep_ service
- Extended the search by `isin` in the _Financial Modeling Prep_ service - Extended the search by `isin` in the _Financial Modeling Prep_ service
- Switched to _ESLint_’s flat config format - Switched to _ESLint_’s flat config format
- Upgraded `eslint` dependencies - Upgraded `eslint` dependencies

182
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -1,4 +1,5 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { import {
DataProviderInterface, DataProviderInterface,
GetDividendsParams, GetDividendsParams,
@ -10,7 +11,6 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { import {
DataProviderInfo, DataProviderInfo,
@ -19,8 +19,14 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import {
AssetClass,
AssetSubClass,
DataSource,
SymbolProfile
} from '@prisma/client';
import { isISIN } from 'class-validator'; import { isISIN } from 'class-validator';
import { countries } from 'countries-list';
import { format, isAfter, isBefore, isSameDay } from 'date-fns'; import { format, isAfter, isBefore, isSameDay } from 'date-fns';
@Injectable() @Injectable()
@ -29,7 +35,8 @@ export class FinancialModelingPrepService implements DataProviderInterface {
private readonly URL = this.getUrl({ version: 3 }); private readonly URL = this.getUrl({ version: 3 });
public constructor( public constructor(
private readonly configurationService: ConfigurationService private readonly configurationService: ConfigurationService,
private readonly cryptocurrencyService: CryptocurrencyService
) { ) {
this.apiKey = this.configurationService.get( this.apiKey = this.configurationService.get(
'API_KEY_FINANCIAL_MODELING_PREP' 'API_KEY_FINANCIAL_MODELING_PREP'
@ -45,10 +52,152 @@ export class FinancialModelingPrepService implements DataProviderInterface {
}: { }: {
symbol: string; symbol: string;
}): Promise<Partial<SymbolProfile>> { }): Promise<Partial<SymbolProfile>> {
return { const response: Partial<SymbolProfile> = {
symbol, symbol,
dataSource: this.getName() dataSource: this.getName()
}; };
try {
if (this.cryptocurrencyService.isCryptocurrency(symbol)) {
const [quote] = await fetch(
`${this.URL}/quote/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
response.assetClass = AssetClass.LIQUIDITY;
response.assetSubClass = AssetSubClass.CRYPTOCURRENCY;
response.currency = symbol.substring(symbol.length - 3);
response.name = quote.name;
} else {
const [assetProfile] = await fetch(
`${this.URL}/profile/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
const { assetClass, assetSubClass } =
this.parseAssetClass(assetProfile);
response.assetClass = assetClass;
response.assetSubClass = assetSubClass;
if (assetSubClass === AssetSubClass.ETF) {
const etfCountryWeightings = await fetch(
`${this.URL}/etf-country-weightings/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
response.countries = etfCountryWeightings.map(
({ country: countryName, weightPercentage }) => {
let countryCode: string;
for (const [code, country] of Object.entries(countries)) {
if (country.name === countryName) {
countryCode = code;
break;
}
}
return {
code: countryCode,
weight: parseFloat(weightPercentage.slice(0, -1)) / 100
};
}
);
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')
)
}
).then((res) => res.json());
if (portfolioDate) {
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')
)
}
).then((res) => res.json());
const sortedTopHoldings = etfHoldings
.sort((a, b) => {
return b.pctVal - a.pctVal;
})
.slice(0, 10);
response.holdings = sortedTopHoldings.map(({ name, pctVal }) => {
return { name, weight: pctVal / 100 };
});
}
const etfSectorWeightings = await fetch(
`${this.URL}/etf-sector-weightings/${symbol}?apikey=${this.apiKey}`,
{
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
response.sectors = etfSectorWeightings.map(
({ sector, weightPercentage }) => {
return {
name: sector,
weight: parseFloat(weightPercentage.slice(0, -1)) / 100
};
}
);
} else if (assetSubClass === AssetSubClass.STOCK) {
if (assetProfile.country) {
response.countries = [{ code: assetProfile.country, weight: 1 }];
}
if (assetProfile.sector) {
response.sectors = [{ name: assetProfile.sector, weight: 1 }];
}
}
response.currency = assetProfile.currency;
if (assetProfile.isin) {
response.isin = assetProfile.isin;
}
response.name = assetProfile.companyName;
if (assetProfile.website) {
response.url = assetProfile.website;
}
}
} catch (error) {
let message = error;
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
).toFixed(3)} seconds`;
}
Logger.error(message, 'FinancialModelingPrepService');
}
return response;
} }
public getDataProviderInfo(): DataProviderInfo { public getDataProviderInfo(): DataProviderInfo {
@ -131,8 +280,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
).then((res) => res.json()); ).then((res) => res.json());
for (const { price, symbol } of quotes) { for (const { price, symbol } of quotes) {
const { currency } = await this.getAssetProfile({ symbol });
response[symbol] = { response[symbol] = {
currency: DEFAULT_CURRENCY, currency,
dataProviderInfo: this.getDataProviderInfo(), dataProviderInfo: this.getDataProviderInfo(),
dataSource: DataSource.FINANCIAL_MODELING_PREP, dataSource: DataSource.FINANCIAL_MODELING_PREP,
marketPrice: price, marketPrice: price,
@ -223,4 +374,25 @@ export class FinancialModelingPrepService implements DataProviderInterface {
private getUrl({ version }: { version: number }) { private getUrl({ version }: { version: number }) {
return `https://financialmodelingprep.com/api/v${version}`; return `https://financialmodelingprep.com/api/v${version}`;
} }
public parseAssetClass(profile: any): {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
} {
let assetClass: AssetClass;
let assetSubClass: AssetSubClass;
if (profile.isEtf) {
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.ETF;
} else if (profile.isFund) {
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.MUTUALFUND;
} else {
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.STOCK;
}
return { assetClass, assetSubClass };
}
} }

Loading…
Cancel
Save