├── README.md ├── LICENSE └── blpapiwrapper.py /README.md: -------------------------------------------------------------------------------- 1 | blpapiwrapper 2 | ============= 3 | 4 | Simple Python wrapper for the Python Open Bloomberg API 5 | 6 | Requisites: 7 | * blpapi Python library (https://www.bloomberg.com/professional/support/api-library/) 8 | * pandas library (http://pandas.pydata.org/) 9 | 10 | This wrapper allows simple use of the Bloomberg Python API, both terminal based and server based (SAPI): 11 | * the terminal version only works if you're connected to Bloomberg, typically on a machine where the Bloomberg terminal application is running and you are logged in; 12 | * the SAPI version needs a Bloomberg SAPI license as well as the details of any user that is logged into the terminal at the time of the request. 13 | 14 | There are three main components: 15 | * a simple implementation that emulates the Excel API bdp and bdh functions, useful for scripting; 16 | * a thread-safe implementation of the Response/Request paradigm; and 17 | * a thread-safe implementation of the Subscription paradigm. 18 | 19 | For the Response/Request paradigm the bdp output comes as a string, the bdh output comes as pandas DataFrame. Check the main() function for examples. 20 | 21 | The Observer pattern is also implemented for the subscription paradigm. 22 | 23 | Tested on Python 2.7 32-bit, Python 3.6.5 64-bit, and Python 3.8 64-bit, with pandas 1.05. 24 | 25 | Note: blpapi installation issue on Windows 10 with Python 3.7: please check my answer on https://stackoverflow.com/questions/52897576/install-error-for-blpapi-in-python-for-bloomberg-api/54186235#54186235 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014-2015 Alexandre Almosni 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /blpapiwrapper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python wrapper to download data through the Bloomberg Open API 3 | Written by Alexandre Almosni alexandre.almosni@gmail.com 4 | (C) 2014-2022 Alexandre Almosni 5 | Released under Apache 2.0 license. More info at http://www.apache.org/licenses/LICENSE-2.0 6 | """ 7 | 8 | from __future__ import print_function 9 | from abc import ABCMeta, abstractmethod 10 | import blpapi 11 | import datetime 12 | import pandas 13 | import threading 14 | from numpy import nan 15 | 16 | #This makes successive requests faster 17 | DATE = blpapi.Name("date") 18 | ERROR_INFO = blpapi.Name("errorInfo") 19 | EVENT_TIME = blpapi.Name("EVENT_TIME") 20 | FIELD_DATA = blpapi.Name("fieldData") 21 | FIELD_EXCEPTIONS = blpapi.Name("fieldExceptions") 22 | FIELD_ID = blpapi.Name("fieldId") 23 | SECURITY = blpapi.Name("security") 24 | SECURITY_DATA = blpapi.Name("securityData") 25 | 26 | BLPAPI_VERSION = float(blpapi.version()[0:4]) # the MessageIterator next() method became hidden starting in blpapi 3.18 27 | ################################################ 28 | 29 | 30 | class BLPSession(blpapi.Session): 31 | """This class is just a wrapper around the blpapi.Session object to allow for SAPI authentication if needed. 32 | host_ip: server IP 33 | host_port: server port 34 | uuid: the user who's authenticating - determines market data permissions 35 | local_ip: the ip of the uuid user - has to be the latest machine the user logged in from""" 36 | def __init__(self, host_ip=None, host_port=None, uuid=None, local_ip=None): 37 | if host_ip is not None: 38 | host_port = int(host_port) # needs to be an int 39 | uuid = int(uuid) # needs to be an int 40 | opt = blpapi.SessionOptions() 41 | opt.setClientMode(2) 42 | opt.setServerHost(host_ip) 43 | opt.setServerPort(host_port) 44 | blpapi.Session.__init__(self, opt) 45 | self.start() 46 | identity = self.createIdentity() 47 | self.openService('//blp/apiauth') 48 | apiAuthSvc = self.getService('//blp/apiauth') 49 | auth_req = apiAuthSvc.createAuthorizationRequest() 50 | auth_req.set('uuid', uuid) 51 | auth_req.set('ipAddress', local_ip) 52 | corr = blpapi.CorrelationId(uuid) 53 | self.sendAuthorizationRequest(auth_req, identity, corr) 54 | while True: 55 | event = self.nextEvent() 56 | if event.eventType() == blpapi.event.Event.RESPONSE: 57 | break 58 | msg_iter = blpapi.event.MessageIterator(event) 59 | auth_msg = msg_iter.__next__().toString()[0:20] if BLPAPI_VERSION > 3.17 else msg_iter.next().toString()[0:20] 60 | if auth_msg == 'AuthorizationSuccess': 61 | print(str(uuid) + ' authorized and connected.') 62 | self.auth_success = True 63 | else: 64 | print(str(uuid) + ' failed to connect.') 65 | self.auth_success = False 66 | else: 67 | blpapi.Session.__init__(self) 68 | self.start() 69 | 70 | 71 | class BLP: 72 | """Naive implementation of the Request/Response Paradigm closely matching the Excel API. 73 | Sharing one session for subsequent requests is faster, however it is not thread-safe, as some events can come faster than others. 74 | bdp returns a string, bdh returns a pandas DataFrame. 75 | This is mostly useful for scripting, but care should be taken when used in a real world application. 76 | Desktop users should not need to use sapi_dic. 77 | """ 78 | 79 | def __init__(self, sapi_dic=None): 80 | if sapi_dic is not None: 81 | self.session = BLPSession(sapi_dic['host_ip'], sapi_dic['host_port'], sapi_dic['uuid'], sapi_dic['local_ip']) 82 | else: 83 | self.session = BLPSession() 84 | self.session.openService('//BLP/refdata') 85 | self.refDataSvc = self.session.getService('//BLP/refdata') 86 | 87 | def bdp(self, strSecurity='US900123AL40 Govt', strData='PX_LAST', strOverrideField='', strOverrideValue=''): 88 | request = self.refDataSvc.createRequest('ReferenceDataRequest') 89 | request.append('securities', strSecurity) 90 | request.append('fields', strData) 91 | 92 | if strOverrideField != '': 93 | o = request.getElement('overrides').appendElement() 94 | o.setElement('fieldId', strOverrideField) 95 | o.setElement('value', strOverrideValue) 96 | 97 | requestID = self.session.sendRequest(request) 98 | 99 | while True: 100 | event = self.session.nextEvent() 101 | if event.eventType() == blpapi.event.Event.RESPONSE: 102 | break 103 | try: 104 | output = blpapi.event.MessageIterator(event).__next__().getElement(SECURITY_DATA).getValueAsElement(0).getElement(FIELD_DATA).getElementAsString(strData) if BLPAPI_VERSION > 3.17 else blpapi.event.MessageIterator(event).next().getElement(SECURITY_DATA).getValueAsElement(0).getElement(FIELD_DATA).getElementAsString(strData) 105 | if output == '#N/A': 106 | output = nan 107 | except: 108 | print('error with '+strSecurity+' '+strData) 109 | output = nan 110 | return output 111 | 112 | def bdh(self, strSecurity='SPX Index', strData='PX_LAST', startdate=datetime.date(2014, 1, 1), enddate=datetime.date(2014, 1, 9), adjustmentSplit=False, periodicity='DAILY', strOverrideField='', strOverrideValue=''): 113 | request = self.refDataSvc.createRequest('HistoricalDataRequest') 114 | request.append('securities', strSecurity) 115 | if type(strData) == str: 116 | strData = [strData] 117 | 118 | for strD in strData: 119 | request.append('fields', strD) 120 | 121 | if strOverrideField != '': 122 | o = request.getElement('overrides').appendElement() 123 | o.setElement('fieldId', strOverrideField) 124 | o.setElement('value', strOverrideValue) 125 | 126 | request.set('startDate', startdate.strftime('%Y%m%d')) 127 | request.set('endDate', enddate.strftime('%Y%m%d')) 128 | request.set('adjustmentSplit', 'TRUE' if adjustmentSplit else 'FALSE') 129 | request.set('periodicitySelection', periodicity) 130 | requestID = self.session.sendRequest(request) 131 | 132 | while True: 133 | event = self.session.nextEvent() 134 | if event.eventType() == blpapi.event.Event.RESPONSE: 135 | break 136 | 137 | fieldDataArray = blpapi.event.MessageIterator(event).__next__().getElement(SECURITY_DATA).getElement(FIELD_DATA) if BLPAPI_VERSION > 3.17 else blpapi.event.MessageIterator(event).next().getElement(SECURITY_DATA).getElement(FIELD_DATA) 138 | fieldDataList = [fieldDataArray.getValueAsElement(i) for i in range(0, fieldDataArray.numValues())] 139 | outDates = [x.getElementAsDatetime(DATE) for x in fieldDataList] 140 | output = pandas.DataFrame(index=outDates, columns=strData) 141 | 142 | for strD in strData: 143 | output[strD] = [x.getElementAsFloat(strD) for x in fieldDataList] 144 | 145 | output.replace('#N/A History', nan, inplace=True) 146 | output.index = pandas.to_datetime(output.index) 147 | return output 148 | 149 | def bdhOHLC(self, strSecurity='SPX Index', startdate=datetime.date(2014, 1, 1), enddate=datetime.date(2014, 1, 9), periodicity='DAILY'): 150 | return self.bdh(strSecurity, ['PX_OPEN', 'PX_HIGH', 'PX_LOW', 'PX_LAST'], startdate, enddate, False, periodicity) 151 | 152 | def closeSession(self): 153 | self.session.stop() 154 | ################################################ 155 | 156 | 157 | class BLPTS: 158 | """Thread-safe implementation of the Request/Response Paradigm. 159 | The functions don't return anything but notify observers of results. 160 | Including startDate as a keyword argument will define a HistoricalDataRequest, otherwise it will be a ReferenceDataRequest. 161 | HistoricalDataRequest sends observers a pandas DataFrame, whereas ReferenceDataRequest sends a pandas Series. 162 | Override seems to only work when there's one security, one field, and one override. 163 | Examples: 164 | BLPTS(['ESA Index', 'VGA Index'], ['BID', 'ASK']) 165 | BLPTS('US900123AL40 Govt','YLD_YTM_BID',strOverrideField='PX_BID',strOverrideValue='200') 166 | BLPTS(['SPX Index','SX5E Index','EUR Curncy'],['PX_LAST','VOLUME'],startDate=datetime.datetime(2014,1,1),endDate=datetime.datetime(2015,5,14),periodicity='DAILY') 167 | BLPTS('AAPL US Equity','SALES_REV_TURN', startDate='CY2010', endDate='CY2018', periodicity='YEARLY') 168 | BLPTS('AAPL US Equity','SALES_REV_TURN', startDate='CY2010', endDate='CY2018', periodicity='YEARLY') 169 | BLPTS('3333 HK Equity','SALES_REV_TURN', startDate='CY2010', endDate='CY2018', periodicity='YEARLY', strOverrideField='EQY_FUND_CRNCY', strOverrideValue='USD') 170 | There seems to be a limit to number of fields we can ask at the same time - less than 20 171 | sapi_dic is used for the SAPI connection 172 | """ 173 | 174 | def __init__(self, securities=[], fields=[], **kwargs): 175 | """ 176 | Keyword arguments: 177 | securities : list of ISINS 178 | fields : list of fields 179 | kwargs : startDate and endDate (datetime.datetime object, note: hours, minutes, seconds, and microseconds must be replaced by 0), periodicity, sapi_dic, etc. 180 | """ 181 | self.kwargs = kwargs 182 | if 'sapi_dic' in self.kwargs and self.kwargs['sapi_dic'] is not None: 183 | self.session = BLPSession(kwargs['sapi_dic']['host_ip'], kwargs['sapi_dic']['host_port'], kwargs['sapi_dic']['uuid'], kwargs['sapi_dic']['local_ip']) 184 | else: 185 | self.session = BLPSession() 186 | self.session.openService('//BLP/refdata') 187 | self.refDataSvc = self.session.getService('//BLP/refdata') 188 | self.observers = [] 189 | if len(securities) > 0 and len(fields) > 0: 190 | # also works if securities and fields are a string 191 | self.fillRequest(securities, fields, **kwargs) 192 | 193 | def fillRequest(self, securities, fields, **kwargs): 194 | """ 195 | keyword arguments: 196 | securities : list of ISINS 197 | fields : list of fields 198 | kwargs : startDate and endDate (datetime.datetime object, note: hours, minutes, seconds, and microseconds must be replaced by 0) 199 | """ 200 | self.kwargs = kwargs 201 | 202 | if type(securities) == str: 203 | securities = [securities] 204 | 205 | if type(fields) == str: 206 | fields = [fields] 207 | 208 | if 'startDate' in kwargs: 209 | self.request = self.refDataSvc.createRequest('HistoricalDataRequest') 210 | self.startDate = kwargs['startDate'] 211 | self.endDate = kwargs['endDate'] 212 | self.periodicity = kwargs['periodicity'] if 'periodicity' in kwargs else 'DAILY' 213 | self.request.set('periodicitySelection', self.periodicity) 214 | # if 'periodicity' in kwargs: 215 | # self.periodicity = kwargs['periodicity'] 216 | # else: 217 | # self.periodicity = 'DAILY' 218 | 219 | if type(self.startDate) == str: 220 | self.request.set('startDate', self.startDate) 221 | else: 222 | self.request.set('startDate', self.startDate.strftime('%Y%m%d')) 223 | 224 | if type(self.endDate) == str: 225 | self.request.set('endDate', self.endDate) 226 | else: 227 | self.request.set('endDate', self.endDate.strftime('%Y%m%d')) 228 | 229 | else: 230 | self.request = self.refDataSvc.createRequest('ReferenceDataRequest') 231 | self.output = pandas.DataFrame(index=securities, columns=fields) 232 | 233 | if 'strOverrideField' in kwargs: 234 | o = self.request.getElement('overrides').appendElement() 235 | o.setElement('fieldId', kwargs['strOverrideField']) 236 | o.setElement('value', kwargs['strOverrideValue']) 237 | 238 | self.securities = securities 239 | self.fields = fields 240 | 241 | for s in securities: 242 | self.request.append('securities', s) 243 | 244 | for f in fields: 245 | self.request.append('fields', f) 246 | 247 | def get(self, newSecurities=[], newFields=[], **kwargs): 248 | """ 249 | securities : list of ISINS 250 | fields : list of fields 251 | kwargs : startDate and endDate (datetime.datetime object, note: hours, minutes, seconds, and microseconds must be replaced by 0) 252 | """ 253 | 254 | if len(newSecurities) > 0 or len(newFields) > 0: 255 | self.fillRequest(newSecurities, newFields, **kwargs) 256 | 257 | self.requestID = self.session.sendRequest(self.request) 258 | 259 | while True: 260 | event = self.session.nextEvent() 261 | if event.eventType() in [blpapi.event.Event.RESPONSE, blpapi.event.Event.PARTIAL_RESPONSE]: 262 | responseSize = blpapi.event.MessageIterator(event).__next__().getElement(SECURITY_DATA).numValues() if BLPAPI_VERSION > 3.17 else blpapi.event.MessageIterator(event).next().getElement(SECURITY_DATA).numValues() 263 | 264 | for i in range(0, responseSize): 265 | 266 | if 'startDate' in self.kwargs: 267 | # HistoricalDataRequest 268 | output = blpapi.event.MessageIterator(event).__next__().getElement(SECURITY_DATA) if BLPAPI_VERSION > 3.17 else blpapi.event.MessageIterator(event).next().getElement(SECURITY_DATA) 269 | security = output.getElement(SECURITY).getValueAsString() 270 | fieldDataArray = output.getElement(FIELD_DATA) 271 | fieldDataList = [fieldDataArray.getValueAsElement(i) for i in range(0, fieldDataArray.numValues())] 272 | dates = map(lambda x: x.getElement(DATE).getValueAsString(), fieldDataList) 273 | outDF = pandas.DataFrame(index=dates, columns=self.fields) 274 | try: 275 | outDF.index = pandas.to_datetime(outDF.index, format='%Y-%m-%d%z') 276 | except: 277 | outDF.index = pandas.to_datetime(outDF.index) 278 | 279 | for field in self.fields: 280 | data = [] 281 | for row in fieldDataList: 282 | if row.hasElement(field): 283 | data.append(row.getElement(field).getValueAsFloat()) 284 | else: 285 | data.append(nan) 286 | 287 | outDF[field] = data 288 | self.updateObservers(security=security, field=field, data=outDF) # update one security one field 289 | 290 | self.updateObservers(security=security, field='ALL', data=outDF) # update one security all fields 291 | 292 | else: 293 | # ReferenceDataRequest 294 | output = blpapi.event.MessageIterator(event).__next__().getElement(SECURITY_DATA).getValueAsElement(i) if BLPAPI_VERSION > 3.17 else blpapi.event.MessageIterator(event).next().getElement(SECURITY_DATA).getValueAsElement(i) 295 | n_elmts = output.getElement(FIELD_DATA).numElements() 296 | security = output.getElement(SECURITY).getValueAsString() 297 | for j in range(0, n_elmts): 298 | data = output.getElement(FIELD_DATA).getElement(j) 299 | field = str(data.name()) 300 | outData = _dict_from_element(data) 301 | self.updateObservers(security=security, field=field, data=outData) # update one security one field 302 | self.output.loc[security, field] = outData 303 | 304 | if n_elmts > 0: 305 | self.updateObservers(security=security, field='ALL', data=self.output.loc[security]) # update one security all fields 306 | else: 307 | print('Empty response received for ' + security) 308 | 309 | if event.eventType() == blpapi.event.Event.RESPONSE: 310 | break 311 | 312 | def register(self, observer): 313 | if not observer in self.observers: 314 | self.observers.append(observer) 315 | 316 | def unregister(self, observer): 317 | if observer in self.observers: 318 | self.observers.remove(observer) 319 | 320 | def unregisterAll(self): 321 | if self.observers: 322 | del self.observers[:] 323 | 324 | def updateObservers(self, *args, **kwargs): 325 | for observer in self.observers: 326 | observer.update(*args, **kwargs) 327 | 328 | def closeSession(self): 329 | self.session.stop() 330 | ################################################ 331 | 332 | 333 | class BLPStream(threading.Thread): 334 | """The Subscription Paradigm 335 | The subscribed data will be sitting in self.output and update automatically. Observers will be notified. 336 | floatInterval is the minimum amount of time before updates - sometimes needs to be set at 0 for things to work properly. In seconds. 337 | intCorrID is a user defined ID for the request 338 | It is sometimes safer to ask for each data (for instance BID and ASK) in a separate stream. 339 | Note that for corporate bonds, a change in the ASK price will still trigger a BID event. 340 | """ 341 | 342 | def __init__(self, strSecurityList=['ESU2 Index', 'VGU2 Index'], strDataList=['BID', 'ASK'], floatInterval=0, intCorrIDList=[0, 1], sapi_dic=None): 343 | threading.Thread.__init__(self) 344 | if sapi_dic is not None: 345 | self.session = BLPSession(BLPSession(sapi_dic['host_ip'], sapi_dic['host_port'], sapi_dic['uuid'], sapi_dic['local_ip'])) 346 | else: 347 | self.session = BLPSession() 348 | self.session.openService("//BLP/mktdata") 349 | 350 | if type(strSecurityList) == str: 351 | strSecurityList = [strSecurityList] 352 | 353 | if type(intCorrIDList) == int: 354 | intCorrIDList = [intCorrIDList] 355 | 356 | if type(strDataList) == str: 357 | strDataList = [strDataList] 358 | 359 | self.strSecurityList = strSecurityList 360 | self.strDataList = strDataList 361 | 362 | if len(strSecurityList) != len(intCorrIDList): 363 | print('Number of securities needs to match number of Correlation IDs, overwriting IDs') 364 | self.intCorrIDList = range(0, len(strSecurityList)) 365 | else: 366 | self.intCorrIDList = intCorrIDList 367 | 368 | self.subscriptionList = blpapi.subscriptionlist.SubscriptionList() 369 | for (security, intCorrID) in zip(self.strSecurityList, self.intCorrIDList): 370 | self.subscriptionList.add(security, self.strDataList, "interval="+str(floatInterval), blpapi.CorrelationId(intCorrID)) 371 | 372 | self.output = pandas.DataFrame(index=self.strSecurityList, columns=self.strDataList) 373 | self.dictCorrID = dict(zip(self.intCorrIDList, self.strSecurityList)) 374 | self.lastUpdateTimeBlmbrg = '' # Warning - if you mix live and delayed data you could have non increasing data 375 | self.lastUpdateTime = datetime.datetime(1900, 1, 1) 376 | self.observers = [] 377 | 378 | def register(self, observer): 379 | if not observer in self.observers: 380 | self.observers.append(observer) 381 | 382 | def unregister(self, observer): 383 | if observer in self.observers: 384 | self.observers.remove(observer) 385 | 386 | def unregisterAll(self): 387 | if self.observers: 388 | del self.observers[:] 389 | 390 | def updateObservers(self, *args, **kwargs): 391 | for observer in self.observers: 392 | observer.update(*args, **kwargs) 393 | 394 | def run(self, verbose=False): 395 | self.session.subscribe(self.subscriptionList) 396 | while True: 397 | event = self.session.nextEvent() 398 | if event.eventType() == blpapi.event.Event.SUBSCRIPTION_DATA: 399 | self.handleDataEvent(event) 400 | else: 401 | if verbose: 402 | self.handleOtherEvent(event) 403 | 404 | def handleDataEvent(self, event): 405 | output = blpapi.event.MessageIterator(event).__next__() if BLPAPI_VERSION > 3.17 else blpapi.event.MessageIterator(event).next() 406 | self.lastUpdateTime = datetime.datetime.now() 407 | corrID = output.correlationIds()[0].value() 408 | security = self.dictCorrID[corrID] 409 | isParsed = False 410 | #print(output.toString()) 411 | 412 | if output.hasElement(EVENT_TIME): 413 | self.lastUpdateTimeBlmbrg = output.getElement(EVENT_TIME).toString() 414 | 415 | for field in self.strDataList: 416 | if output.hasElement(field): 417 | isParsed = True 418 | try: 419 | data = output.getElement(field).getValueAsFloat() 420 | except: 421 | data = nan 422 | print('error: ',security,field)#,output.getElement(field).getValueAsString() # this can still error if field is there but is empty 423 | self.output.loc[security, field] = data 424 | self.updateObservers(time=self.lastUpdateTime, security=security, field=field, corrID=corrID, data=data, bbgTime=self.lastUpdateTimeBlmbrg) 425 | 426 | # It can happen that you get an event without the data behind the event! 427 | self.updateObservers(time=self.lastUpdateTime, security=security, field='ALL', corrID=corrID, data=0, bbgTime=self.lastUpdateTimeBlmbrg) 428 | # if not isParsed: 429 | # print(output.toString()) 430 | 431 | def handleOtherEvent(self, event): 432 | output = blpapi.event.MessageIterator(event).__next__() if BLPAPI_VERSION > 3.17 else blpapi.event.MessageIterator(event).next() 433 | msg = output.toString() 434 | if event.eventType() == blpapi.event.Event.AUTHORIZATION_STATUS: 435 | print("Authorization event: " + msg) 436 | elif event.eventType() == blpapi.event.Event.SUBSCRIPTION_STATUS: 437 | print("Subscription status event: " + msg) 438 | else: 439 | print("Other event: event "+str(event.eventType())) 440 | 441 | def closeSubscription(self): 442 | self.session.unsubscribe(self.subscriptionList) 443 | ################################################ 444 | #Convenience functions below#################### 445 | ################################################ 446 | 447 | 448 | def _dict_from_element(element): 449 | ''' 450 | Used for e.g. dividends 451 | ''' 452 | try: 453 | return element.getValueAsString() 454 | except: 455 | if element.numValues() > 1: 456 | results = [] 457 | for i in range(0, element.numValues()): 458 | subelement = element.getValue(i) 459 | name = str(subelement.name()) 460 | results.append(_dict_from_element(subelement)) 461 | else: 462 | results = {} 463 | for j in range(0, element.numElements()): 464 | subelement = element.getElement(j) 465 | name = str(subelement.name()) 466 | results[name] = _dict_from_element(subelement) 467 | return results 468 | 469 | 470 | class Observer(object): 471 | __metaclass__ = ABCMeta 472 | 473 | @abstractmethod 474 | def update(self, *args, **kwargs): 475 | pass 476 | 477 | 478 | class HistoryWatcher(Observer): 479 | """Object to stream and record history data from Bloomberg. 480 | """ 481 | def __init__(self): 482 | self.outputDC = {} 483 | 484 | def update(self, *args, **kwargs): 485 | if kwargs['field'] != 'ALL': 486 | self.outputDC[(kwargs['security'], kwargs['field'])]=kwargs['data'][[kwargs['field']]]#double brackets keep it a dataframe, not a series 487 | 488 | 489 | def simpleReferenceDataRequest(id_to_ticker_dic, fields, sapi_dic=None): 490 | ''' 491 | Common use case for reference data request 492 | id_to_ticker_dic: dictionnary with user id mapped to Bloomberg security ticker e.g. {'Apple':'AAPL US Equity'} 493 | Returns a dataframe indexed by the user id, with columns equal to fields 494 | ''' 495 | ticker_to_id_dic = {v: k for k, v in id_to_ticker_dic.items()} 496 | blpts = BLPTS(id_to_ticker_dic.values(), fields, sapi_dic=sapi_dic) 497 | blpts.get() 498 | blpts.closeSession() 499 | blpts.output['id'] = blpts.output.index 500 | blpts.output['id'].replace(ticker_to_id_dic,inplace=True) 501 | blpts.output.set_index('id', inplace=True) 502 | return blpts.output.copy() 503 | 504 | 505 | def simpleHistoryRequest(securities=[], fields=[], startDate=datetime.datetime(2015,1,1), endDate=datetime.datetime(2016,1,1), **kwargs): 506 | ''' 507 | Convenience function to retrieve historical data for a list of securities and fields 508 | As returned data can have different length, missing data will be replaced with nan (note it's already taken care of in one security several fields) 509 | If multiple securities and fields, a MultiIndex dataframe will be returned. 510 | ''' 511 | blpts = BLPTS(securities, fields, startDate=startDate, endDate=endDate, **kwargs) 512 | historyWatcher = HistoryWatcher() 513 | blpts.register(historyWatcher) 514 | blpts.get() 515 | blpts.closeSession() 516 | #for key,df in historyWatcher.outputDC.iteritems(): 517 | for key, df in historyWatcher.outputDC.items(): 518 | df.columns = [key] 519 | output = pandas.concat(historyWatcher.outputDC.values(), axis=1) 520 | output.columns = pandas.MultiIndex.from_tuples(output.columns) 521 | output.columns.names = ['Security', 'Field'] 522 | return output 523 | 524 | 525 | ################################################ 526 | #Examples below################################# 527 | ################################################ 528 | 529 | def excelEmulationExample(): 530 | ##Examples of the Request/Response Paradigm 531 | bloomberg = BLP() 532 | print(bloomberg.bdp()) 533 | print('') 534 | print(bloomberg.bdp('US900123AL40 Govt', 'YLD_YTM_BID', 'PX_BID', '200')) 535 | print('') 536 | print(bloomberg.bdh()) 537 | print('') 538 | print(bloomberg.bdhOHLC()) 539 | bloomberg.closeSession() 540 | 541 | 542 | class ObserverStreamExample(Observer): 543 | def update(self, *args, **kwargs): 544 | output = kwargs['time'].strftime("%Y-%m-%d %H:%M:%S") + ' received ' + kwargs['security'] + ' ' + kwargs['field'] + '=' + str(kwargs['data']) 545 | output = output + '. CorrID '+str(kwargs['corrID']) + ' bbgTime ' + kwargs['bbgTime'] 546 | print(output) 547 | 548 | 549 | def streamPatternExample(): 550 | stream = BLPStream('ESZ2 Index', ['BID', 'ASK'], 0, 1) 551 | #stream=BLPStream('XS1151974877 CORP',['BID','ASK'],0,1) #Note that for a bond only BID gets updated even if ASK moves. 552 | obs = ObserverStreamExample() 553 | stream.register(obs) 554 | stream.start() 555 | 556 | 557 | class ObserverRequestExample(Observer): 558 | def update(self, *args, **kwargs): 559 | if kwargs['field'] == 'ALL': 560 | print(kwargs['security']) 561 | print(kwargs['data']) 562 | 563 | 564 | def BLPTSExample(): 565 | result = BLPTS(['XS0316524130 Corp', 'US900123CG37 Corp'], ['PX_BID', 'INT_ACC', 'DAYS_TO_NEXT_COUPON']) 566 | result.get() 567 | print(result.output) 568 | result.closeSession() 569 | 570 | 571 | ############################################################################# 572 | 573 | 574 | def main(): 575 | pass 576 | 577 | if __name__ == '__main__': 578 | main() 579 | --------------------------------------------------------------------------------