Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for the Bloomberg equity screening BEQS() function, wit… #9

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions pdblp/pdblp.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,175 @@ def stop(self):
Close the blp session
"""
self.session.stop()

def beqs(self, screen_name, screen_type='PRIVATE', group='General', language_id='ENGLISH', asof_date=None):
"""
The beqs() function allows users to retrieve a table of data for a selected equity screen
that was created using the Equity Screening (EQS) function.
See https://data.bloomberglp.com/professional/sites/4/BLPAPI-Core-Developer-Guide.pdf.
Make a Beqs request, get tickers and fields, return long
pandas Dataframe with columns [ticker, field, value]

Parameters
----------
screen_name: string
String corresponding to name of screen
screen_type: 'GLOBAL' or 'PRIVATE'
Indicates that the screen is a Bloomberg-created sample screen (GLOBAL) or
a saved custom screen that users have created (PRIVATE).
group: string
If the screens are organized into groups, allows users to define the name of the group that
contains the screen. If the users use a Bloomberg sample screen,
they must use this parameter to specify the name of the folder in which the screen appears.
For example, group="Investment Banking" when importing the “Cash/Debt Ratio” screen.
language_id: string
Allows users to override the EQS report header language
asof_date: datetime or None
Allows users to backdate the screen, so they can analyze the historical results on the screen
"""

data = self._beqs(screen_name, screen_type, group, language_id, asof_date)

data = DataFrame(data)
data.columns = ["ticker", "field", "value", "date"]
return data

def _beqs(self, screen_name, screen_type, group, language_id, asof_date):

# flush event queue in case previous call errored out
while (self.session.tryNextEvent()):
pass

request = self._beqs_create_req(screen_name, screen_type, group, language_id)

cid = None
if asof_date is not None:
overrides = request.getElement("overrides")
ovrd = overrides.appendElement()
ovrd.setElement("fieldId", "PiTDate")
ovrd.setElement("value", asof_date.strftime('%Y%m%d'))
cid = blpapi.CorrelationId(asof_date)

logging.debug("Sending Request:\n %s" % request)
# Send the request
self.session.sendRequest(request, correlationId=cid)
data = []
# Process received events
while (True):
# We provide timeout to give the chance for Ctrl+C handling:
ev = self.session.nextEvent(500)
data = self._beqs_parse_event(data, ev)

if ev.eventType() == blpapi.Event.RESPONSE:
# Response completely received, so we could exit
break

return data

def _beqs_parse_event(self, data, ev):
for msg in ev:
logging.debug("Message Received:\n %s" % msg)
corrID = msg.correlationIds()[0].value()
fldData = msg.getElement('data').getElement('securityData')
for i in range(fldData.numValues()):
ticker = (fldData.getValue(i).getElement("security").getValue()) # NOQA
reqFldsData = (fldData.getValue(i).getElement('fieldData'))
for e in reqFldsData.elements():
fld = str(e.name())
# this is for dealing with requests which return arrays
# of values for a single field
if reqFldsData.getElement(fld).isArray():
lrng = reqFldsData.getElement(fld).numValues()
for k in range(lrng):
elms = (reqFldsData.getElement(fld).getValue(k).elements()) # NOQA
# if the elements of the array have multiple
# subelements this will just append them all
# into a list
for elm in elms:
data.append([ticker, fld, elm.getValue(), corrID])
else:
val = reqFldsData.getElement(fld).getValue()
data.append([ticker, fld, val, corrID])
return data

def _beqs_create_req(self, screen_name, screen_type, group, language_id):
request = self.refDataService.createRequest('BeqsRequest')
request.set('screenName', screen_name)
request.set('screenType', screen_type)
request.set('Group', group)
request.set('languageId', language_id)
return request

def beqs_hist(self, screen_name, screen_type='PRIVATE', group='General', language_id='ENGLISH',
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

freq parameter here is unused and should be removed. longdata parameter should be documented.

asof_dates=pd.date_range(datetime.date.today(), datetime.date.today(), freq='b'),
timeout=2000, longdata=False):
"""
Get tickers and fields, periodically override ASOF to create
a time series. Return pandas dataframe with column MultiIndex
of tickers and fields, Index otherwise.
If single field, DataFrame is ordered same as tickers,
otherwise MultiIndex is sorted.

Parameters
----------
screen_name: string
String corresponding to name of screen
screen_type: 'GLOBAL' or 'PRIVATE'
Indicates that the screen is a Bloomberg-created sample screen (GLOBAL) or
a saved custom screen that users have created (PRIVATE).
group: string
If the screens are organized into groups, allows users to define the name of the group that
contains the screen. If the users use a Bloomberg sample screen,
they must use this parameter to specify the name of the folder in which the screen appears.
For example, group="Investment Banking" when importing the “Cash/Debt Ratio” screen.
language_id: string
Allows users to override the EQS report header language
asof_dates: DatetimeIndex
Dates on which to get the screen's historical results
timeout: int
Passed into nextEvent(timeout), number of milliseconds before
timeout occurs
"""
# correlationIDs should be unique to a session so rather than
# managing unique IDs for the duration of the session just restart
# a session for each call
self.restart()

request = self._beqs_create_req(screen_name, screen_type, group, language_id)
overrides = request.getElement("overrides")
ovrd = overrides.appendElement()
for dt in asof_dates:
ovrd.setElement("fieldId", "PiTDate")
ovrd.setElement("value", dt.strftime('%Y%m%d'))
# CorrelationID used to keep track of which response coincides with which request
cid = blpapi.CorrelationId(dt)
logging.debug("Sending Request:\n %s" % request)
self.session.sendRequest(request, correlationId=cid)
data = []
# Process received events
ev_counter = 0
while (True):
ev = self.session.nextEvent(timeout)
data = self._beqs_parse_event(data, ev)
if ev.eventType() == blpapi.Event.RESPONSE:
ev_counter += 1
if ev_counter == len(asof_dates):
break
# if ev.eventType() == blpapi.Event.TIMEOUT:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commented out section should be removed

# All events processed
# if (len(data) / len(set(data[0])) / len(set(data[1]))) == len(asof_dates):
# break
# else:
# raise(RuntimeError("Timeout, increase timeout parameter"))
data = pd.DataFrame(data)
data.columns = ['ticker', 'field', 'value', 'date']
data = data.sort_values(by=['date', 'ticker', 'field'])
data = data.reset_index(drop=True)
data = data.loc[:, ['date', 'ticker', 'field', 'value']]

if not longdata:
cols = ['ticker', 'field']
data = data.set_index(['date'] + cols).unstack('field')
data.columns = data.columns.droplevel(0)

return data
24 changes: 20 additions & 4 deletions pdblp/tests/test_pdblp.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import datetime
import unittest
import pandas as pd
from pandas.util.testing import assert_frame_equal
from pandas.util.testing import assertIsInstance
from pdblp import pdblp

IP_PORT = 8194
Expand Down Expand Up @@ -94,18 +94,18 @@ def test_ref_one_ticker_one_field(self):
def test_ref_one_ticker_one_field_many_output(self):
df = self.con.ref('CL1 Comdty', 'FUT_CHAIN')
# unknown / changing data returned so just assert right type
assertIsInstance(df, pd.DataFrame)
assert isinstance(df, pd.DataFrame)

def test_ref_two_ticker_one_field_many_output(self):
df = self.con.ref(['CL1 Comdty', 'CO1 Comdty'], 'FUT_CHAIN')
# unknown / changing data returned so just assert right type
assertIsInstance(df, pd.DataFrame)
assert isinstance(df, pd.DataFrame)

def test_ref_two_ticker_two_field_many_output(self):
df = self.con.ref(['CL1 Comdty', 'CO1 Comdty'],
['FUT_CHAIN', 'FUT_CUR_GEN_TICKER'])
# unknown / changing data returned so just assert right type
assertIsInstance(df, pd.DataFrame)
assert isinstance(df, pd.DataFrame)

def test_ref_one_ticker_one_field_override(self):
df = self.con.ref('AUD Curncy', 'SETTLE_DT',
Expand Down Expand Up @@ -164,3 +164,19 @@ def test_hist_ref_one_ticker_one_field_pivoted_non_numeric(self):
)
df_expect.index.names = ["date"]
assert_frame_equal(df, df_expect)

def test_beqs_current(self):
df = self.con.beqs('Insider Buyers', 'GLOBAL', 'Popular')
assert len(df) > 0

def test_beqs_asof(self):
asof_date = datetime.datetime(2016, 5, 4)
df = self.con.beqs('Insider Buyers', 'GLOBAL', 'Popular', asof_date=asof_date)
assert len(df) == 1145

def test_beqs_hist(self):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test is failing for me since I am getting a DataFrame of length 488. Bloombergs exact data returned is not always reliable, a test which just checks the known properties of the returned DataFrame would be better (e.g. a DataFrame with specific columns)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello Matthew,

I have read your codes and also the codes from blpapi. I found it is impossible to get the data of backdates. Actually I want to realize the lookup function that use the market cap rank in 2019 to select stock. But I cannot find any api methods to do that. I have found yours codes is working, but it is now not accurate?

Beth

asof_dates = pd.DatetimeIndex([datetime.datetime(2016, 5, 4),
datetime.datetime(2017, 5, 10)])
df = self.con.beqs_hist('Insider Buyers', 'GLOBAL', 'Popular',
asof_dates=asof_dates, longdata=False)
assert len(df) == 487