Feature/extend asset profile data in financial modeling prep service (#4206)
* Extend asset profile data * Update changelog
This commit is contained in:
parent
40b628e0e7
commit
511a2d6d0d
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Extended the asset profile data in the _Financial Modeling Prep_ service
|
||||
- Extended the search by `isin` in the _Financial Modeling Prep_ service
|
||||
- Switched to _ESLint_’s flat config format
|
||||
- Upgraded `eslint` dependencies
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
|
||||
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
|
||||
import {
|
||||
DataProviderInterface,
|
||||
GetDividendsParams,
|
||||
@ -10,7 +11,6 @@ import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
} from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DataProviderInfo,
|
||||
@ -19,8 +19,14 @@ import {
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
|
||||
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 { countries } from 'countries-list';
|
||||
import { format, isAfter, isBefore, isSameDay } from 'date-fns';
|
||||
|
||||
@Injectable()
|
||||
@ -29,7 +35,8 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
private readonly URL = this.getUrl({ version: 3 });
|
||||
|
||||
public constructor(
|
||||
private readonly configurationService: ConfigurationService
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly cryptocurrencyService: CryptocurrencyService
|
||||
) {
|
||||
this.apiKey = this.configurationService.get(
|
||||
'API_KEY_FINANCIAL_MODELING_PREP'
|
||||
@ -45,10 +52,152 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
}: {
|
||||
symbol: string;
|
||||
}): Promise<Partial<SymbolProfile>> {
|
||||
return {
|
||||
const response: Partial<SymbolProfile> = {
|
||||
symbol,
|
||||
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 {
|
||||
@ -131,8 +280,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
).then((res) => res.json());
|
||||
|
||||
for (const { price, symbol } of quotes) {
|
||||
const { currency } = await this.getAssetProfile({ symbol });
|
||||
|
||||
response[symbol] = {
|
||||
currency: DEFAULT_CURRENCY,
|
||||
currency,
|
||||
dataProviderInfo: this.getDataProviderInfo(),
|
||||
dataSource: DataSource.FINANCIAL_MODELING_PREP,
|
||||
marketPrice: price,
|
||||
@ -223,4 +374,25 @@ export class FinancialModelingPrepService implements DataProviderInterface {
|
||||
private getUrl({ version }: { version: number }) {
|
||||
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…
x
Reference in New Issue
Block a user