diff --git a/CHANGELOG.md b/CHANGELOG.md index e32a20c45..0fe6d0aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the _OpenFIGI_ data enhancer for _Financial Instrument Global Identifier_ (FIGI) +- Added `figi`, `figiComposite` and `figiShareClass` to the asset profile model + ### Changed - Moved the fees on account level feature from experimental to general availability diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 83d062b83..8fd35f8dd 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -280,6 +280,9 @@ export class ImportService { createdAt, currency, dataSource, + figi, + figiComposite, + figiShareClass, id, isin, name, @@ -350,6 +353,9 @@ export class ImportService { createdAt, currency, dataSource, + figi, + figiComposite, + figiShareClass, id, isin, name, @@ -509,6 +515,9 @@ export class ImportService { comment: null, countries: null, createdAt: undefined, + figi: null, + figiComposite: null, + figiShareClass: null, id: undefined, isin: null, name: null, diff --git a/apps/api/src/services/configuration/configuration.service.ts b/apps/api/src/services/configuration/configuration.service.ts index 40a04f5a0..b355d5a20 100644 --- a/apps/api/src/services/configuration/configuration.service.ts +++ b/apps/api/src/services/configuration/configuration.service.ts @@ -38,6 +38,7 @@ export class ConfigurationService { JWT_SECRET_KEY: str({}), MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }), MAX_ITEM_IN_CACHE: num({ default: 9999 }), + OPEN_FIGI_API_KEY: str({ default: '' }), PORT: port({ default: 3333 }), RAPID_API_API_KEY: str({ default: '' }), REDIS_HOST: str({ default: 'localhost' }), diff --git a/apps/api/src/services/data-gathering/data-gathering.service.ts b/apps/api/src/services/data-gathering/data-gathering.service.ts index 34645b9ea..78531b745 100644 --- a/apps/api/src/services/data-gathering/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering/data-gathering.service.ts @@ -164,6 +164,9 @@ export class DataGatheringService { countries, currency, dataSource, + figi, + figiComposite, + figiShareClass, isin, name, sectors, @@ -178,6 +181,9 @@ export class DataGatheringService { countries, currency, dataSource, + figi, + figiComposite, + figiShareClass, isin, name, sectors, @@ -189,6 +195,9 @@ export class DataGatheringService { assetSubClass, countries, currency, + figi, + figiComposite, + figiShareClass, isin, name, sectors, diff --git a/apps/api/src/services/data-provider/data-enhancer/data-enhancer.module.ts b/apps/api/src/services/data-provider/data-enhancer/data-enhancer.module.ts index 069309508..23d64ac86 100644 --- a/apps/api/src/services/data-provider/data-enhancer/data-enhancer.module.ts +++ b/apps/api/src/services/data-provider/data-enhancer/data-enhancer.module.ts @@ -1,5 +1,6 @@ import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module'; +import { OpenFigiDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/openfigi/openfigi.service'; import { TrackinsightDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/trackinsight/trackinsight.service'; import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service'; import { Module } from '@nestjs/common'; @@ -9,6 +10,7 @@ import { DataEnhancerService } from './data-enhancer.service'; @Module({ exports: [ DataEnhancerService, + OpenFigiDataEnhancerService, TrackinsightDataEnhancerService, YahooFinanceDataEnhancerService, 'DataEnhancers' @@ -16,15 +18,21 @@ import { DataEnhancerService } from './data-enhancer.service'; imports: [ConfigurationModule, CryptocurrencyModule], providers: [ DataEnhancerService, + OpenFigiDataEnhancerService, TrackinsightDataEnhancerService, YahooFinanceDataEnhancerService, { inject: [ + OpenFigiDataEnhancerService, TrackinsightDataEnhancerService, YahooFinanceDataEnhancerService ], provide: 'DataEnhancers', - useFactory: (trackinsight, yahooFinance) => [trackinsight, yahooFinance] + useFactory: (openfigi, trackinsight, yahooFinance) => [ + openfigi, + trackinsight, + yahooFinance + ] } ] }) diff --git a/apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts b/apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts new file mode 100644 index 000000000..363cbb167 --- /dev/null +++ b/apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts @@ -0,0 +1,85 @@ +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; +import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config'; +import { parseSymbol } from '@ghostfolio/common/helper'; +import { Injectable } from '@nestjs/common'; +import { SymbolProfile } from '@prisma/client'; +import got, { Headers } from 'got'; + +@Injectable() +export class OpenFigiDataEnhancerService implements DataEnhancerInterface { + private static baseUrl = 'https://api.openfigi.com'; + + public constructor( + private readonly configurationService: ConfigurationService + ) {} + + public async enhance({ + response, + symbol + }: { + response: Partial; + symbol: string; + }): Promise> { + if ( + !( + response.assetClass === 'EQUITY' && + (response.assetSubClass === 'ETF' || response.assetSubClass === 'STOCK') + ) + ) { + return response; + } + + const headers: Headers = {}; + const { exchange, ticker } = parseSymbol({ + symbol, + dataSource: response.dataSource + }); + + if (this.configurationService.get('OPEN_FIGI_API_KEY')) { + headers['X-OPENFIGI-APIKEY'] = + this.configurationService.get('OPEN_FIGI_API_KEY'); + } + + let abortController = new AbortController(); + + setTimeout(() => { + abortController.abort(); + }, DEFAULT_REQUEST_TIMEOUT); + + const mappings = await got + .post(`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, { + headers, + json: [{ exchCode: exchange, idType: 'TICKER', idValue: ticker }], + // @ts-ignore + signal: abortController.signal + }) + .json(); + + if (mappings?.length === 1 && mappings[0].data?.length === 1) { + const { compositeFIGI, figi, shareClassFIGI } = mappings[0].data[0]; + + if (figi) { + response.figi = figi; + } + + if (compositeFIGI) { + response.figiComposite = compositeFIGI; + } + + if (shareClassFIGI) { + response.figiShareClass = shareClassFIGI; + } + } + + return response; + } + + public getName() { + return 'OPENFIGI'; + } + + public getTestSymbol() { + return undefined; + } +} diff --git a/apps/api/src/services/interfaces/environment.interface.ts b/apps/api/src/services/interfaces/environment.interface.ts index b437668ab..9b10a3205 100644 --- a/apps/api/src/services/interfaces/environment.interface.ts +++ b/apps/api/src/services/interfaces/environment.interface.ts @@ -26,6 +26,7 @@ export interface Environment extends CleanedEnvAccessors { JWT_SECRET_KEY: string; MAX_ACTIVITIES_TO_IMPORT: number; MAX_ITEM_IN_CACHE: number; + OPEN_FIGI_API_KEY: string; PORT: number; RAPID_API_API_KEY: string; REDIS_HOST: string; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 03fc250b8..3a3ee6e11 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -322,6 +322,15 @@ export function parseDate(date: string): Date | null { return parseISO(date); } +export function parseSymbol({ dataSource, symbol }: UniqueAsset) { + const [ticker, exchange] = symbol.split('.'); + + return { + ticker, + exchange: exchange ?? (dataSource === 'YAHOO' ? 'US' : undefined) + }; +} + export function prettifySymbol(aSymbol: string): string { return aSymbol?.replace(ghostfolioScraperApiSymbolPrefix, ''); } diff --git a/prisma/migrations/20231021094346_added_figi_figi_composite_and_figi_share_class_to_symbol_profile/migration.sql b/prisma/migrations/20231021094346_added_figi_figi_composite_and_figi_share_class_to_symbol_profile/migration.sql new file mode 100644 index 000000000..eb8abbe5a --- /dev/null +++ b/prisma/migrations/20231021094346_added_figi_figi_composite_and_figi_share_class_to_symbol_profile/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "SymbolProfile" +ADD COLUMN "figi" TEXT, +ADD COLUMN "figiComposite" TEXT, +ADD COLUMN "figiShareClass" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a4d4e028e..015431e6c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -132,6 +132,9 @@ model SymbolProfile { createdAt DateTime @default(now()) currency String dataSource DataSource + figi String? + figiComposite String? + figiShareClass String? id String @id @default(uuid()) isin String? name String?