In financial markets, tradable instruments and securities have unique identifiers. The identifiers are very useful, because you can make sure that you and your counterparty are talking about the same instrument while trading. The difficulty is that there isn't really a standard for all the various sorts of instruments or markets. Anyone working in the industry will recognize this issue, especially people working at larger institutions who deal with a variety of instruments. Products like equities, bonds (fixed income), indices, derivatives, currencies and structured products all have their own conventions. Just to name a few identifiers, you may come across:
Fortunately, the problem is recognized and there are steps being made to tackle this. Bloomberg has initiated OpenFigi and Refinitiv (formerly Thomson Reuters) PermID. Basically, they are open sourcing their proprietary identifiers. This makes it easier to map instruments to other identifiers which you may use.
Please take a look at the following websites, before continuing this tutorial:
In this tutorial, we access OpenFIGI and PermID through the APIs and find for five companies traded at the Frankfurt Stock Exchange the full company name, FIGI, PermID, Legal Entity Identifier (LEI), initial public offering date, physical address, website and the company's phone number.
For OpenFIGI we will use some of the example code which is published
As always, let's start with importing some packages:
import requests # 2.19.1
import json # 2.0.9
import pandas as pd # 0.23.4
import pprint
We will get the identifiers for a handful of companies listed in the DAX (Deutscher Aktienindex) in Germany. It is a blue chip stock market index consisting of the 30 major German companies trading on the Frankfurt Stock Exchange. We randomly chose 5 tickers, feel free to change the list or add any you may like.
tickers = ['ADS', 'BAS', 'DTE', 'SAP', 'SIE'] # Adidas, BASF, Deutsche Telekom, SAP & Siemens
The Market Identifier Code (MIC) for Xetra in Frankfurt Germany XETR
jobs = [
{'idType': 'TICKER', 'idValue': 'ADS', 'micCode': 'XETR'},
{'idType': 'TICKER', 'idValue': 'BAS', 'micCode': 'XETR'},
{'idType': 'TICKER', 'idValue': 'DTE', 'micCode': 'XETR'},
{'idType': 'TICKER', 'idValue': 'SAP', 'micCode': 'XETR'},
{'idType': 'TICKER', 'idValue': 'SIE', 'micCode': 'XETR'}
]
# Alternatively, if you start with ISIN codes:
# jobs = [{'idType': 'ID_ISIN', 'idValue': 'DE0005557508', 'micCode': 'XETR'}]
Now we have defined the jobs, we need to set some of the configurations and define the function we can use for the jobs. You don't necessarily need an API key for FIGI, but with
openfigi_apikey = '' # Please put your own API Key here
openfigi_url = 'https://api.openfigi.com/v2/mapping'
openfigi_headers = {'Content-Type': 'text/json'}
def map_jobs(jobs):
if openfigi_apikey:
openfigi_headers['X-OPENFIGI-APIKEY'] = openfigi_apikey
response = requests.post(url=openfigi_url, headers=openfigi_headers,
json=jobs)
if response.status_code != 200:
raise Exception('Bad response code {}'.format(str(response.status_code)))
return response.json()
Now let's execute the function and print the results to the screen.
job_results = map_jobs(jobs)
pprint.pprint(job_results)
[{'data': [{'compositeFIGI': 'BBG000FR1Q22', 'exchCode': 'GY', 'figi': 'BBG000FR1RP5', 'marketSector': 'Equity', 'name': 'ADIDAS AG', 'securityDescription': 'ADS', 'securityType': 'Common Stock', 'securityType2': 'Common Stock', 'shareClassFIGI': 'BBG001S8J8Q3', 'ticker': 'ADS', 'uniqueID': 'EQ0018000100001000', 'uniqueIDFutOpt': None}]}, {'data': [{'compositeFIGI': 'BBG000BBVT77', 'exchCode': 'GY', 'figi': 'BBG000BBVVK7', 'marketSector': 'Equity', 'name': 'BASF SE', 'securityDescription': 'BAS', 'securityType': 'Common Stock', 'securityType2': 'Common Stock', 'shareClassFIGI': 'BBG001S5SYB8', 'ticker': 'BAS', 'uniqueID': 'EQ0011569400001000', 'uniqueIDFutOpt': None}]}, {'data': [{'compositeFIGI': 'BBG000HJTKL0', 'exchCode': 'GY', 'figi': 'BBG000HJTMS9', 'marketSector': 'Equity', 'name': 'DEUTSCHE TELEKOM AG-REG', 'securityDescription': 'DTE', 'securityType': 'Common Stock', 'securityType2': 'Common Stock', 'shareClassFIGI': 'BBG001S5T4S5', 'ticker': 'DTE', 'uniqueID': 'EQ0018055600001000', 'uniqueIDFutOpt': None}]}, {'data': [{'compositeFIGI': 'BBG000BG7DY8', 'exchCode': 'GY', 'figi': 'BBG000BG7GX2', 'marketSector': 'Equity', 'name': 'SAP SE', 'securityDescription': 'SAP', 'securityType': 'Common Stock', 'securityType2': 'Common Stock', 'shareClassFIGI': 'BBG001S6RK27', 'ticker': 'SAP', 'uniqueID': 'EQ0011841200001000', 'uniqueIDFutOpt': None}]}, {'data': [{'compositeFIGI': 'BBG000BCCRV3', 'exchCode': 'GY', 'figi': 'BBG000BCCT76', 'marketSector': 'Equity', 'name': 'SIEMENS AG-REG', 'securityDescription': 'SIE', 'securityType': 'Common Stock', 'securityType2': 'Common Stock', 'shareClassFIGI': 'BBG001S69GV4', 'ticker': 'SIE', 'uniqueID': 'EQ0011574600001000', 'uniqueIDFutOpt': None}]}]
As you can see, the result of the mapping job is a list of dictionaries. For every ticker, the 'key' is called "data". Let's change the structure a little bit, so we can easily convert it to Pandas DataFrame
just_dictionaries = [d['data'][0] for d in job_results]
With this new list of dictionaries, we can directly convert it:
df_figi = pd.DataFrame.from_dict(just_dictionaries)
df_figi
compositeFIGI | figi | name | securityType2 | shareClassFIGI | ticker | uniqueIDFutOpt | ||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | BBG000FR1Q22 | GY | BBG000FR1RP5 | Equity | ADIDAS AG | ADS | Common Stock | Common Stock | BBG001S8J8Q3 | ADS | EQ0018000100001000 | None |
1 | BBG000BBVT77 | GY | BBG000BBVVK7 | Equity | BASF SE | BAS | Common Stock | Common Stock | BBG001S5SYB8 | BAS | EQ0011569400001000 | None |
2 | BBG000HJTKL0 | GY | BBG000HJTMS9 | Equity | DEUTSCHE TELEKOM AG-REG | DTE | Common Stock | Common Stock | BBG001S5T4S5 | DTE | EQ0018055600001000 | None |
3 | BBG000BG7DY8 | GY | BBG000BG7GX2 | Equity | SAP SE | SAP | Common Stock | Common Stock | BBG001S6RK27 | SAP | EQ0011841200001000 | None |
4 | BBG000BCCRV3 | GY | BBG000BCCT76 | Equity | SIEMENS AG-REG | SIE | Common Stock | Common Stock | BBG001S69GV4 | SIE | EQ0011574600001000 | None |
The DataFrame actually contains a little bit more information than we need for this tutorial. We can filter the interesting columns and set the tickers to be the index.
# Columns of interest
columns = ['ticker', 'name', 'marketSector', 'figi', 'uniqueID']
# Filter out the columns of interest
df_figi = df_figi[columns]
# Set the tickers to be the index
df_figi = df_figi.set_index('ticker')
# Show the DataFrame
df_figi
name | figi | |||
---|---|---|---|---|
ticker | ||||
ADS | ADIDAS AG | Equity | BBG000FR1RP5 | EQ0018000100001000 |
BAS | BASF SE | Equity | BBG000BBVVK7 | EQ0011569400001000 |
DTE | DEUTSCHE TELEKOM AG-REG | Equity | BBG000HJTMS9 | EQ0018055600001000 |
SAP | SAP SE | Equity | BBG000BG7GX2 | EQ0011841200001000 |
SIE | SIEMENS AG-REG | Equity | BBG000BCCT76 | EQ0011574600001000 |
PermID is short for "permanent identifier". It is the open sourced identifier system of Thomson Reuters. You do need to register at the Developer Portal to be able to use their APIs. In this tutorial, we will use Record Matching API
NB: At the time of writing, it is not possible to use ISINs as input nor to get RICs as output. Hopefully, this will be added to the API's functionality in the future.
Please fill in your own access token before continuing:
# Your own access token
access_token = ''
Plus we define the configuration:
# API endpoint
request_url = "https://api.thomsonreuters.com/permid/match"
headers = {
'Content-Type': 'text/plain',
'Accept': 'application/json',
'x-ag-access-token': access_token,
'x-openmatch-numberOfMatchesPerRecord': '1', # only return 1 match per ticker
'x-openmatch-dataType': 'Organization', # only match to "organizations", not "persons" or other tags
}
Now it gets a little bit more tricky. As you can read in the documentation, the API excepts a CSV file or "specially formatted text" as input. We are working with a list of tickers in this tutorial, so we need to construct the "specially formatted text" ourselves.
# The first line in the text field is 'Standard Identifier'. We use 'Ticker' as identifier in this tutorial
text_field = 'Standard Identifier\n'
# For every ticker, we will add a new line and specify the Market Identifier Code (MIC) / Exchange
exchange = 'XETR'
for ticker in tickers:
identifier = 'TICKER:' + ticker + '&&MIC:' + exchange + '\n'
text_field += identifier
# Print the 'text_field' to screen for inspection
print(text_field)
Standard Identifier TICKER:ADS&&MIC:XETR TICKER:BAS&&MIC:XETR TICKER:DTE&&MIC:XETR TICKER:SAP&&MIC:XETR TICKER:SIE&&MIC:XETR
Now it is time to construct & make the actual request.
response = requests.post(request_url, headers=headers, data=text_field)
r = response.json()
The information are looking for is in 'outputContentResponse'. We can print it to the screen for inspection.
# Pretty print the response to the screen
pprint.pprint(r['outputContentResponse'])
[{'Input_Standard Identifier': 'TICKER:ADS&&MIC:XETR', 'Match Level': 'Excellent', 'Match OpenPermID': 'https://permid.org/1-4295868725', 'Match Ordinal': '1', 'Match OrgName': 'Adidas AG', 'Match Score': '100%', 'Original Row Number': '2', 'ProcessingStatus': 'OK'}, {'Input_Standard Identifier': 'TICKER:BAS&&MIC:XETR', 'Match Level': 'Excellent', 'Match OpenPermID': 'https://permid.org/1-4295869198', 'Match Ordinal': '1', 'Match OrgName': 'BASF SE', 'Match Score': '100%', 'Original Row Number': '3', 'ProcessingStatus': 'OK'}, {'Input_Standard Identifier': 'TICKER:DTE&&MIC:XETR', 'Match Level': 'Excellent', 'Match OpenPermID': 'https://permid.org/1-4295870332', 'Match Ordinal': '1', 'Match OrgName': 'Deutsche Telekom AG', 'Match Score': '100%', 'Original Row Number': '4', 'ProcessingStatus': 'OK'}, {'Input_Standard Identifier': 'TICKER:SAP&&MIC:XETR', 'Match Level': 'Excellent', 'Match OpenPermID': 'https://permid.org/1-5043321284', 'Match Ordinal': '1', 'Match OrgName': 'SAP SE', 'Match Score': '100%', 'Original Row Number': '5', 'ProcessingStatus': 'OK'}, {'Input_Standard Identifier': 'TICKER:SIE&&MIC:XETR', 'Match Level': 'Excellent', 'Match OpenPermID': 'https://permid.org/1-4295869238', 'Match Ordinal': '1', 'Match OrgName': 'Siemens AG', 'Match Score': '100%', 'Original Row Number': '6', 'ProcessingStatus': 'OK'}]
Let's quickly loop through the results to check out the PermID's.
for company in r['outputContentResponse']:
print(company['Match OrgName'] + ' --> ' + company['Match OpenPermID'])
Adidas AG --> https://permid.org/1-4295868725 BASF SE --> https://permid.org/1-4295869198 Deutsche Telekom AG --> https://permid.org/1-4295870332 SAP SE --> https://permid.org/1-5043321284 Siemens AG --> https://permid.org/1-4295869238
Did you check out any of the links? PermID offers some additional data besides the identifier. Let's define a function which will help us to retrieve the additional data.
def permid_data(permid_url):
permid_headers = {
'Accept': 'text/turtle',
}
permid_params = {
'format': 'json-ld',
'access-token': access_token
}
# The actual request
permid_response = requests.get(permid_url, headers=headers, params=permid_params)
# Convert the response to JSON
permid_data = json.loads(permid_response.content)
return permid_data
Now we will create a dictionary of dictionaries, one for every ticker we are interested in. We do this so we can then later convert it easily to Pandas DataFrame
# Create an empty dictionary
permid_dict = {}
# Loop through all tickers and put the data in the dictionary
for ticker, i in zip(tickers, r['outputContentResponse']):
# The PermID url for the ticker from the response earlier
permid_url = i['Match OpenPermID']
# Use the function defined above to download the data
data = permid_data(permid_url)
# Put the desired data in a dictionary for the ticker
permid_dict[ticker] = {
'company': data['vcard:organization-name'],
'IPO' : data['hasIPODate'],
'address': data['mdaas:HeadquartersAddress'],
'website': data['hasURL'],
'phone' : data['tr-org:hasHeadquartersPhoneNumber'],
'LEI' : data['tr-org:hasLEI'],
'permid' : data['tr-common:hasPermId'],
'permid_url' : permid_url
}
All the data is now in a dictionary of dictionaries. Again, it is also very easy to convert this to Pandas DataFrame
df_permid = pd.DataFrame.from_dict(permid_dict, orient='index') # Orient='index' for data in rows instead of columns
df_permid
company | IPO | address | website | phone | LEI | permid_url | ||
---|---|---|---|---|---|---|---|---|
ADS | Adidas AG | 1997-11-28T05:00:00Z | Adi-Dassler-Strasse 1\nHERZOGENAURACH\nBAYERN\... | https://www.adidas-group.com/de/ | 499132840 | 549300JSX0Z4CW0V5023 | 4295868725 | https://permid.org/1-4295868725 |
BAS | BASF SE | 1952-01-30T05:00:00Z | Carl-Bosch-Str. 38\nLUDWIGSHAFEN AM RHEIN\nRHE... | https://www.basf.com/ | 49621600 | 529900PM64WH8AF1E917 | 4295869198 | https://permid.org/1-4295869198 |
DTE | Deutsche Telekom AG | 2000-01-24T05:00:00Z | Friedrich-Ebert-Allee 140\nBONN\nNORDRHEIN-WES... | https://www.telekom.com/ | 492281810 | 549300V9QSIG4WX4GJ96 | 4295870332 | https://permid.org/1-4295870332 |
SAP | SAP SE | 1988-11-04T05:00:00Z | Dietmar-Hopp-Allee 16\nWALLDORF\nBADEN-WUERTTE... | https://www.sap.com/ | 496227747474 | 529900D6BF99LW9R2E68 | 5043321284 | https://permid.org/1-5043321284 |
SIE | Siemens AG | 2008-02-25T05:00:00Z | Wittelsbacherplatz 2\nMUENCHEN\nBAYERN\n80333\... | https://www.siemens.com/global/de/ | 498963600 | W38RGI023J3WT1HWRP32 | 4295869238 | https://permid.org/1-4295869238 |
As a final touch, let's join both DataFrames:
df_final = df_figi.join(df_permid)
df_final
name | figi | company | IPO | address | website | phone | LEI | permid_url | ||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
ticker | ||||||||||||
ADS | ADIDAS AG | Equity | BBG000FR1RP5 | EQ0018000100001000 | Adidas AG | 1997-11-28T05:00:00Z | Adi-Dassler-Strasse 1\nHERZOGENAURACH\nBAYERN\... | https://www.adidas-group.com/de/ | 499132840 | 549300JSX0Z4CW0V5023 | 4295868725 | https://permid.org/1-4295868725 |
BAS | BASF SE | Equity | BBG000BBVVK7 | EQ0011569400001000 | BASF SE | 1952-01-30T05:00:00Z | Carl-Bosch-Str. 38\nLUDWIGSHAFEN AM RHEIN\nRHE... | https://www.basf.com/ | 49621600 | 529900PM64WH8AF1E917 | 4295869198 | https://permid.org/1-4295869198 |
DTE | DEUTSCHE TELEKOM AG-REG | Equity | BBG000HJTMS9 | EQ0018055600001000 | Deutsche Telekom AG | 2000-01-24T05:00:00Z | Friedrich-Ebert-Allee 140\nBONN\nNORDRHEIN-WES... | https://www.telekom.com/ | 492281810 | 549300V9QSIG4WX4GJ96 | 4295870332 | https://permid.org/1-4295870332 |
SAP | SAP SE | Equity | BBG000BG7GX2 | EQ0011841200001000 | SAP SE | 1988-11-04T05:00:00Z | Dietmar-Hopp-Allee 16\nWALLDORF\nBADEN-WUERTTE... | https://www.sap.com/ | 496227747474 | 529900D6BF99LW9R2E68 | 5043321284 | https://permid.org/1-5043321284 |
SIE | SIEMENS AG-REG | Equity | BBG000BCCT76 | EQ0011574600001000 | Siemens AG | 2008-02-25T05:00:00Z | Wittelsbacherplatz 2\nMUENCHEN\nBAYERN\n80333\... | https://www.siemens.com/global/de/ | 498963600 | W38RGI023J3WT1HWRP32 | 4295869238 | https://permid.org/1-4295869238 |
We started with only the tickers for a handful of companies. Finally, we have for every ticker the full company name, FIGI, PermID, Legal Entity Identifier (LEI), initial public offering date, physical address, website
Not bad, right?
About the author:
Joris H., Python & open source enthusiast. Entrepreneur @ PythonSherpa - https://www.pythonsherpa.com