├── requirements.txt ├── requirements_dev.txt ├── openfigi ├── __init__.py ├── __main__.py └── openfigi.py ├── .gitignore ├── m ├── Makefile ├── setup.py ├── LICENSE ├── tests └── test_basic.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | click 2 | requests -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | twine 2 | wheel 3 | black -------------------------------------------------------------------------------- /openfigi/__init__.py: -------------------------------------------------------------------------------- 1 | from .openfigi import OpenFigi, BASE_URLS 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .idea 3 | openfigi.egg-info 4 | openfigi/__pycache__ 5 | __pycache__ 6 | dist 7 | build 8 | *.rst 9 | -------------------------------------------------------------------------------- /m: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e; set -u 4 | 5 | rm -f dist/* 6 | python setup.py bdist_wheel sdist 7 | twine upload dist/* 8 | 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export PYTHONPATH = . 2 | 3 | black-format: 4 | black -t py38 -S -l 120 openfigi tests setup.py 5 | 6 | black: black-format 7 | 8 | test: 9 | pytest -v tests 10 | 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('LICENSE') as f: 4 | license = f.read() 5 | 6 | setup( 7 | name='openfigi', 8 | version='0.0.9', 9 | description='A simple wrapper for openfigi.com', 10 | author='Julian Wergieluk', 11 | author_email='julian@wergieluk.com', 12 | url='https://github.com/jwergieluk/openfigi', 13 | license=license, 14 | packages=find_packages(), 15 | install_requires=['requests', 'click'], 16 | classifiers=[ 17 | 'Development Status :: 4 - Beta', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Programming Language :: Python :: 3', 20 | ], 21 | entry_points={'console_scripts': ['ofg = openfigi.__main__:call_figi']}, 22 | ) 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017 Julian Wergieluk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_basic.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import openfigi 3 | 4 | 5 | class MyTestCase(unittest.TestCase): 6 | def test_invalid_api_type(self): 7 | self.assertRaises(ValueError, openfigi.OpenFigi, api_version="V0") 8 | 9 | def test_wkn_ticker_anonymous_v1(self): 10 | ofg = openfigi.OpenFigi(api_version="V1") 11 | self.assertIn("v1", ofg.url) 12 | self.assertNotIn("v2", ofg.url) 13 | ofg.enqueue_request(id_type='ID_WERTPAPIER', id_value='A0YEDG') 14 | 15 | response = ofg.fetch_response() 16 | self.assertTrue(type(response) is list) 17 | self.assertTrue(len(response) > 0) 18 | self.assertTrue(type(response[0]) is dict) 19 | self.assertTrue('data' in response[0].keys()) 20 | self.assertTrue(len(response[0]['data']) > 0) 21 | 22 | def test_wkn_ticker_anonymous_v2(self): 23 | """Get an ETF by WKN and check if response makes sense""" 24 | ofg = openfigi.OpenFigi(api_version="V2") 25 | self.assertIn("v2", ofg.url) 26 | self.assertNotIn("v1", ofg.url) 27 | ofg.enqueue_request(id_type='ID_WERTPAPIER', id_value='A0YEDG') 28 | 29 | response = ofg.fetch_response() 30 | self.assertTrue(type(response) is list) 31 | self.assertTrue(len(response) > 0) 32 | self.assertTrue(type(response[0]) is dict) 33 | self.assertTrue('data' in response[0].keys()) 34 | self.assertTrue(len(response[0]['data']) > 0) 35 | 36 | 37 | if __name__ == '__main__': 38 | unittest.main() 39 | -------------------------------------------------------------------------------- /openfigi/__main__.py: -------------------------------------------------------------------------------- 1 | from openfigi import OpenFigi, BASE_URLS 2 | import click 3 | import logging 4 | import os 5 | import json 6 | 7 | 8 | root_logger = logging.getLogger('') 9 | root_logger.setLevel(logging.INFO) 10 | console = logging.StreamHandler() 11 | formatter = logging.Formatter('# %(asctime)s %(levelname)s: %(name)s: %(message)s', datefmt='%Y%m%d %H:%M:%S') 12 | console.setFormatter(formatter) 13 | root_logger.addHandler(console) 14 | 15 | 16 | @click.command() 17 | @click.argument('id_type', nargs=1) 18 | @click.argument('id_values', nargs=-1) 19 | @click.option('--exchange-code', default='', help='An optional exchange code if it applies (cannot use with mic_code).') 20 | @click.option( 21 | '--mic-code', 22 | default='', 23 | help='An optional ISO market identification code(MIC) if it applies (cannot use with exchange_code).', 24 | ) 25 | @click.option('--currency', default='', help='An optional currency if it applies.') 26 | @click.option('--remove-missing/--no-remove-missing', default=False, help='Remove records with errors.') 27 | @click.option( 28 | '--api-version', 29 | default='V1', 30 | help="The OpenFIGI API version to utilize.", 31 | type=click.Choice(list(BASE_URLS.keys())), 32 | ) 33 | def call_figi(id_type, id_values, exchange_code, mic_code, currency, remove_missing, api_version): 34 | """Calls OpenFIGI API 35 | 36 | ID_TYPE must be one of the following: 37 | BASE_TICKER COMPOSITE_ID_BB_GLOBAL ID_BB ID_BB_8_CHR ID_BB_GLOBAL ID_BB_GLOBAL_SHARE_CLASS_LEVEL ID_BB_SEC_NUM_DES 38 | ID_BB_UNIQUE ID_CINS ID_COMMON ID_CUSIP ID_CUSIP_8_CHR ID_EXCH_SYMBOL ID_FULL_EXCHANGE_SYMBOL ID_ISIN ID_ITALY 39 | ID_SEDOL ID_SHORT_CODE ID_TRACE ID_WERTPAPIER OCC_SYMBOL OPRA_SYMBOL TICKER TRADING_SYSTEM_IDENTIFIER 40 | UNIQUE_ID_FUT_OPT 41 | 42 | ID_VALUES is a list of (space separated) ids corresponding to the specified ID_TYPE. 43 | """ 44 | key = None 45 | if 'openfigi_key' in os.environ: 46 | key = os.environ['openfigi_key'] 47 | else: 48 | root_logger.info('openfigi_key variable not present in the environment. Using anonymous access.') 49 | figi = OpenFigi(key, api_version=api_version) 50 | for id_value in id_values: 51 | figi.enqueue_request(id_type.upper(), id_value, exchange_code.upper(), mic_code.upper(), currency.upper()) 52 | text = figi.fetch_response(remove_missing) 53 | click.echo(json.dumps(text, sort_keys=True, indent=4)) 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openfigi 2 | 3 | Simple wrapper and a command-line tool for Bloomberg's OpenFIGI API. 4 | 5 | The API specification is located at https://www.openfigi.com/api 6 | 7 | ## Installation 8 | 9 | Execute 10 | 11 | pip install openfigi 12 | 13 | or clone this repository and install the package directly from disk: 14 | 15 | git clone https://github.com/jwergieluk/openfigi.git 16 | cd openfigi 17 | pip install . 18 | 19 | ## Usage 20 | 21 | >>> import openfigi 22 | >>> 23 | >>> conn = openfigi.OpenFigi("32577205-8353-4cb9-b11e-3b9bbfd1fde2") 24 | >>> conn.enqueue_request(id_type='ID_WERTPAPIER', id_value='XM91CQ', mic_code='EUWX') 25 | >>> print(conn.fetch_response()) 26 | [{'idValue': 'XM91CQ', 'micCode': 'EUWX', 'idType': 'ID_WERTPAPIER', 'data': [{'shareClassFIGI': None, 'uniqueIDFutOpt': None, 'uniqueID': 'EQ0000000047042754', 'name': 'DEUTSCH-PW17 DAX INDEX', 'figi': 'BBG00BP732P7', 'exchCode': 'GW', 'marketSector': 'Equity', 'securityType': 'Index WRT', 'ticker': 'XM91CQ', 'securityType2': 'Warrant', 'securityDescription': 'XM91CQ', 'compositeFIGI': 'BBG00BP73295'}]}] 27 | 28 | 29 | ## Cli usage 30 | 31 | > ofg --help 32 | Usage: ofg [OPTIONS] ID_TYPE [ID_VALUES]... 33 | 34 | Calls OpenFIGI API 35 | 36 | ID_TYPE must be one of the following: BASE_TICKER COMPOSITE_ID_BB_GLOBAL 37 | ID_BB ID_BB_8_CHR ID_BB_GLOBAL ID_BB_GLOBAL_SHARE_CLASS_LEVEL 38 | ID_BB_SEC_NUM_DES ID_BB_UNIQUE ID_CINS ID_COMMON ID_CUSIP ID_CUSIP_8_CHR 39 | ID_EXCH_SYMBOL ID_FULL_EXCHANGE_SYMBOL ID_ISIN ID_ITALY ID_SEDOL 40 | ID_SHORT_CODE ID_TRACE ID_WERTPAPIER OCC_SYMBOL OPRA_SYMBOL TICKER 41 | TRADING_SYSTEM_IDENTIFIER UNIQUE_ID_FUT_OPT 42 | 43 | ID_VALUES is a list of (space separated) ids corresponding to the specified 44 | ID_TYPE. 45 | 46 | Options: 47 | --exchange-code TEXT An optional exchange code if it applies 48 | (cannot use with mic_code). 49 | --mic-code TEXT An optional ISO market identification 50 | code(MIC) if it applies (cannot use with 51 | exchange_code). 52 | --currency TEXT An optional currency if it applies. 53 | --remove-missing / --no-remove-missing 54 | Remove records with errors. 55 | --api-version [V1|V2] The OpenFIGI API version to utilize. 56 | --help Show this message and exit. 57 | 58 | Sample call: 59 | 60 | $ ofg --mic-code EUWX ID_WERTPAPIER XM91CQ 61 | [ 62 | { 63 | "data": [ 64 | { 65 | "compositeFIGI": "BBG00BP73295", 66 | "exchCode": "GW", 67 | "figi": "BBG00BP732P7", 68 | "marketSector": "Equity", 69 | "name": "DEUTSCH-PW17 DAX INDEX", 70 | "securityType": "Index WRT", 71 | "shareClassFIGI": null, 72 | "ticker": "XM91CQ", 73 | "uniqueID": "EQ0000000047042754", 74 | "uniqueIDFutOpt": null 75 | } 76 | ] 77 | } 78 | ] 79 | 80 | The cli tool searches for the `openfigi_key` environment variable and uses it to 81 | authenticate the API calls. If `openfigi_key` is not defined, an anonymous access is used. 82 | 83 | ## Trademarks 84 | 85 | 'OPENFIGI', 'BLOOMBERG', and 'BLOOMBERG.COM' are trademarks and service marks of 86 | Bloomberg Finance L.P., a Delaware limited partnership, or its subsidiaries. 87 | 88 | ## Copyright and license 89 | 90 | MIT License: see LICENSE file for details. 91 | 92 | Copyright (c) 2016-2021 Julian Wergieluk 93 | -------------------------------------------------------------------------------- /openfigi/openfigi.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import requests 3 | import time 4 | 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | BASE_URLS = {"V1": "https://api.openfigi.com/v1/mapping", "V2": "https://api.openfigi.com/v2/mapping"} 9 | 10 | 11 | class OpenFigi: 12 | """This class tries to map the interface defined in https://www.openfigi.com/api""" 13 | 14 | id_types = { 15 | "BASE_TICKER": "An indistinct identifier which may be linked to multiple instruments. May need to be combined with other values to identify a unique instrument.", 16 | "COMPOSITE_ID_BB_GLOBAL": "Composite Financial Instrument Global Identifier - The Composite Financial Instrument Global Identifier (FIGI) enables users to link multiple FIGIs at the trading venue level within the same country or market in order to obtain an aggregated view for an instrument within that country or market.", 17 | "ID_BB": "A legacy Bloomberg identifier.", 18 | "ID_BB_8_CHR": "A legacy Bloomberg identifier (8 characters only).", 19 | "ID_BB_GLOBAL": "Financial Instrument Global Identifier (FIGI) - An identifier that is assigned to instruments of all asset classes and is unique to an individual instrument. Once issued, the FIGI assigned to an instrument will not change.", 20 | "ID_BB_GLOBAL_SHARE_CLASS_LEVEL": "Share Class Financial Instrument Global Identifier - A Share Class level Financial Instrument Global Identifier is assigned to an instrument that is traded in more than one country. This enables users to link multiple Composite FIGIs for the same instrument in order to obtain an aggregated view for that instrument across all countries (globally).", 21 | "ID_BB_SEC_NUM_DES": "Security ID Number Description - Descriptor for a financial instrument. Similar to the ticker field, but will provide additional metadata data.", 22 | "ID_BB_UNIQUE": "Unique Bloomberg Identifier - A legacy, internal Bloomberg identifier.", 23 | "ID_CINS": "CINS - CUSIP International Numbering System.", 24 | "ID_COMMON": "Common Code - A nine digit identification number.", 25 | "ID_CUSIP": "CUSIP - Committee on Uniform Securities Identification Procedures.", 26 | "ID_CUSIP_8_CHR": "CUSIP (8 Characters Only) - Committee on Uniform Securities Identification Procedures.", 27 | "ID_EXCH_SYMBOL": "Local Exchange Security Symbol - Local exchange security symbol.", 28 | "ID_FULL_EXCHANGE_SYMBOL": "Full Exchange Symbol - Contains the exchange symbol for futures, options, indices inclusive of base symbol and other security elements.", 29 | "ID_ISIN": "ISIN - International Securities Identification Number.", 30 | "ID_ITALY": "Italian Identifier Number - The Italian Identification number consisting of five or six digits.", 31 | "ID_SEDOL": "Sedol Number - Stock Exchange Daily Official List.", 32 | "ID_SHORT_CODE": "An exchange venue specific code to identify fixed income instruments primarily traded in Asia.", 33 | "ID_TRACE": "Trace eligible bond identifier issued by FINRA.", 34 | "ID_WERTPAPIER": "Wertpapierkennnummer/WKN - German securities identification code.", 35 | "OCC_SYMBOL": "OCC Symbol - A twenty-one character option symbol standardized by the Options Clearing Corporation (OCC) to identify a U.S. option.", 36 | "OPRA_SYMBOL": "OPRA Symbol - Option symbol standardized by the Options Price Reporting Authority (OPRA) to identify a U.S. option.", 37 | "TICKER": "Ticker - Ticker is a specific identifier for a financial instrument that reflects common usage.", 38 | "TRADING_SYSTEM_IDENTIFIER": "Trading System Identifier - Unique identifier for the instrument as used on the source trading system.", 39 | "UNIQUE_ID_FUT_OPT": "Unique Identifier for Future Option - Bloomberg unique ticker with logic for index, currency, single stock futures, commodities and commodity options.", 40 | } 41 | 42 | def __init__(self, key=None, api_version="V1"): 43 | if api_version not in BASE_URLS: 44 | raise ValueError("Unsupported API version. Supported versions: {0}" "".format(", ".join(BASE_URLS.keys()))) 45 | self.url = BASE_URLS[api_version] 46 | self.api_key = key 47 | self.headers = {'Content-Type': 'text/json', 'X-OPENFIGI-APIKEY': key} 48 | self.request_items = [] 49 | self.response_items = [] 50 | self.max_tickers_per_request = 100 51 | 52 | def enqueue_request(self, id_type, id_value, exchange_code='', mic_code='', currency=''): 53 | query = {'idType': id_type, 'idValue': id_value} 54 | if id_type not in self.id_types.keys(): 55 | self.logger.error('Bad id_type.') 56 | return 57 | if len(exchange_code) > 0: 58 | query['exchCode'] = exchange_code 59 | if len(mic_code) > 0: 60 | query['micCode'] = mic_code 61 | if len(currency) > 0: 62 | query['currency'] = currency 63 | 64 | self.request_items.append(query) 65 | 66 | def _get_batch(self, batch_request_items, remove_missing=False): 67 | response = requests.post(self.url, json=batch_request_items, headers=self.headers) 68 | try: 69 | response.raise_for_status() 70 | except requests.HTTPError: 71 | if response.status_code == 400: 72 | logger.error('The request body is not an array.') 73 | if response.status_code == 401: 74 | logger.error('The API_KEY is invalid.') 75 | if response.status_code == 404: 76 | logger.error('The requested path is invalid.') 77 | if response.status_code == 405: 78 | logger.error('The HTTP verb is not POST.') 79 | if response.status_code == 406: 80 | logger.error('The server does not support the requested Accept type.') 81 | if response.status_code == 413: 82 | logger.error('The request exceeds the max number of identifiers support in one request.') 83 | if response.status_code == 429: 84 | logger.error('Too Many Requests.') 85 | if response.status_code == 500: 86 | logger.error('Internal Server Error.') 87 | return None 88 | 89 | batch_response_items = response.json() 90 | if len(batch_response_items) == len(batch_request_items): 91 | for (i, item) in enumerate(batch_response_items): 92 | item.update(batch_request_items[i]) 93 | else: 94 | logger.warning('Number of request and response items do not match. Dumping the results only.') 95 | if remove_missing: 96 | for item in batch_response_items: 97 | if 'error' not in item.keys(): 98 | self.response_items.append(item) 99 | else: 100 | self.response_items += batch_response_items 101 | 102 | def fetch_response(self, remove_missing=False): 103 | """Partitions the requests into batches and attempts to get responses. 104 | 105 | See https://www.openfigi.com/api#rate-limiting for a detailed explanation. 106 | """ 107 | if len(self.request_items) < 100: 108 | self._get_batch(self.request_items, remove_missing) 109 | else: 110 | self._get_batch(self.request_items[-100:], remove_missing) 111 | self.request_items = self.request_items[:-100] 112 | time.sleep(0.6) 113 | self.fetch_response(remove_missing) 114 | 115 | self.request_items.clear() 116 | return self.response_items 117 | --------------------------------------------------------------------------------