Investigating the effect of Company Announcements on their Share Price following COVID-19 (using the S&P 500)
A lot of company valuation speculation has come about since the COrona-VIrus-Disease-2019 (COVID-19 or COVID for short) started to impact the stock market (estimated on the 20th of February 2020, 2020-02-20). Many investors tried to estimate the impact of the outbreak on businesses and trade accordingly as fast as possible. In this haste, it is possible that they miss-priced the effect of COVID on certain stocks.
This article lays out a framework to investigate whether the Announcement of Financial Statements after COVID (id est (i.e.): after 2020-02-20) impacted the price of stocks in any specific industry sector. It will proceed simply by producing a graph of the movement in average daily close prices for each industry - averaged from the time each company produced a Post COVID Announcement (i.e.: after they first produced a Financial Statement after 2020-02-20).
From there, one may stipulate that a profitable investment strategy could consist in going long in stocks of companies (i) that did not release an announcement since COVID yet (ii) within a sector that the framework bellow suggest will probably increase in price following from such an announcement.
Thomson Reuters Eikon with access to new Eikon Data APIs.
pickle: If one wishes to copy and manipulate this code, 'pickling' data along the way should aid in making sure no data is lost when / in case there are kernel issues.
First we can use the library ' platform ' to show which version of Python we are using
# The ' from ... import ' structure here allows us to only import the module ' python_version ' from the library ' platform ': from platform import python_version print("This code runs on Python version " + python_version())
This code runs on Python version 3.7.7
We use Refinitiv's Eikon Python Application Programming Interface (API) to access financial data. We can access it via the Python library "eikon" that can be installed simply by using pip install.
import eikon as ek # The key is placed in a text file so that it may be used in this code without showing it itself: eikon_key = open("eikon.txt","r") ek.set_app_key(str(eikon_key.read())) # It is best to close the files we opened in order to make sure that we don't stop any other services/programs from accessing them if they need to: eikon_key.close()
The following are Python-built-in modules/librarys, therefore they do not have specific version numbers.
# datetime will allow us to manipulate Western World dates import datetime # dateutil will allow us to manipulate dates in equations import dateutil
numpy is needed for datasets' statistical and mathematical manipulations
import numpy print("The numpy library imported in this code is version: " + numpy.__version__)
The numpy library imported in this code is version: 1.18.2
pandas will be needed to manipulate data sets
import pandas # This line will ensure that all columns of our dataframes are always shown: pandas.set_option('display.max_columns', None) print("The pandas library imported in this code is version: " + pandas.__version__)
The pandas library imported in this code is version: 1.0.3
matplotlib is needed to plot graphs of all kinds
import matplotlib # the use of ' as ... ' (specifically here: ' as plt ') allows us to create a shorthand for a module (here: ' matplotlib.pyplot ') import matplotlib.pyplot as plt print("The matplotlib library imported in this code is version: " + matplotlib.__version__)
The matplotlib library imported in this code is version: 3.2.1
'plot1ax' was defined to plot data on one y axis (as opposed to two, one on the right and one on the left).
def plot1ax(dataset, ylabel = "", title = "", xlabel = "Year", datasubset = , # datasubset needs to be a list of the number of each column within the dtaset that needs to be labelled on the left datarange = False, # If wanting to plot graph from and to a specific point, make datarange a list of start and end date linescolor = False, # This needs to be a list of the color of each vector to be plotted, in order they are shown in their dataframe from left to right figuresize = (12,4), # This can be changed to give graphs of different proportions. It is defaulted to a 12 by 4 (ratioed) graph facecolor="0.25",# This allows the user to change the background color as needed grid = True, # This allows us to decide whether or not to include a grid in our graphs time_index = , time_index_step = 48, # These two variables allow us to dictate the frequency of the ticks on the x-axis of our graph legend = True):
'Get_Daily_Close' is a function that adds a series of daily close prices to the dataframe named 'daily_df' and plots it.
# Defining the ' daily_df ' variable before the ' Get_Daily_Close ' function daily_df = pandas.DataFrame() def Get_Daily_Close(instrument, # Name of the instrument in a list. days_back, # Number of days from which to collect the data. plot_title = False, # If ' = True ', then a graph of the data will be shown. plot_time_index_step = 30 * 3, # This line dictates the index frequency on the graph/plot's x axis. col = ""): # This can be changed to name the column of the merged dataframe.
'Get_Announcement_For_Index' is a function setup that gets Eikon recorded Company Announcement Data through time for any index (or instrument).
def Get_Announcement_For_Index(index_instrument, periods_back, show_df = False, show_list = False):
Setting Up Dates
Before starting to investigate data pre- or post-COVID, we need to define the specific time when COVID affected stock markets: In this instance we chose "2020-02-20"
COVID_start_date = datetime.datetime.strptime("2020-02-20", '%Y-%m-%d').date() days_since_COVID = (datetime.date.today() - COVID_start_date).days
The bellow collects announcements of companies within the index of choice for the past 3 financial periods. In this article, the Standard & Poor's 500 Index (S&P500 or SPX for short) is used as an example. It can be used with indices such as FTSE or DJI instead of the SPX.
index_Announcement_df, index_Announcement_list = Get_Announcement_For_Index(index_instrument = ["0#.SPX"],periods_back = 3, show_df = False,show_list = False)
Now we can choose only announcements post COVID.
Announcement_COVID_date =  for k in (1,2,3): index_Instruments_COVID_date =  index_Announcement_post_COVID_list =  for i in range(len(index_Announcement_list)): index_Instrument_COVID_date =  for j in reversed(index_Announcement_list[i].iloc[:,1]): try: # Note that ' if (index_Announcement_list[i].iloc[1,1] - COVID_start_date).days >= 0: ' would not work if (datetime.datetime.strptime(index_Announcement_list[i].iloc[:,1].iloc[-1], '%Y-%m-%d').date() - COVID_start_date).days >= 0: while len(index_Instrument_COVID_date) == 0: if (datetime.datetime.strptime(j, '%Y-%m-%d').date() - datetime.datetime.strptime("2020-02-20", '%Y-%m-%d').date()).days >= 0: index_Instrument_COVID_date.append(j) else: index_Instrument_COVID_date.append("NaT") except: index_Instrument_COVID_date.append("NaT") index_Instruments_COVID_date.append(index_Instrument_COVID_date) Instruments_Announcement_COVID_date = pandas.DataFrame(index_Instruments_COVID_date, index = index_Announcement_df.Instrument.unique(), columns = ["Date"]) Instruments_Announcement_COVID_date.Date = pandas.to_datetime(Instruments_Announcement_COVID_date.Date) Announcement_COVID_date.append(Instruments_Announcement_COVID_date)
Instruments_Income_Statement_Announcement_COVID_date = Announcement_COVID_date Instruments_Income_Statement_Announcement_COVID_date.columns = ["Date of the First Income Statement Announced after COVID"]
Instruments_Cash_Flow_Statement_Announcement_COVID_date = Announcement_COVID_date Instruments_Cash_Flow_Statement_Announcement_COVID_date.columns = ["Date of the First Cash Flow Statement Announced after COVID"]
Instruments_Balance_Sheet_COVID_date = Announcement_COVID_date Instruments_Balance_Sheet_COVID_date.columns = ["Date of the First Balance Sheet Announced after COVID"]
The cell bellow collects Daily Close Prices for all relevant instruments in the index chosen.
for i in index_Announcement_df.iloc[:,0].unique(): Get_Daily_Close(i, days_back = days_since_COVID)
Some instruments might have been added to the index midway during our time period of choice. They are the ones bellow:
removing = [i.split() + " Close Price" for i in daily_df.iloc[0,:][daily_df.iloc[0,:].isna() == True].index] print("We will be removing " + str(removing) + " from our dataframe")
['CARR.N Close Price', 'OTIS.N Close Price']
The cell bellow will remove them to make sure that the do not skew our statistics later on in the code.
# This line removes instruments that wera added midway to the index daily_df_no_na = daily_df.drop(removing, axis = 1).dropna()
Now we can focus on stock price movements alone.
daily_df_trend = pandas.DataFrame(columns = daily_df_no_na.columns) for i in range(len(pandas.DataFrame.transpose(daily_df_no_na))): daily_df_trend.iloc[:,i] = daily_df_no_na.iloc[:,i] - daily_df_no_na.iloc[0,i]
The following 3 cells display plots to visualise our data thus far.
datasubset_list =  for i in range(len(daily_df_no_na.columns)): datasubset_list.append(i)
plot1ax(dataset = daily_df_no_na, ylabel = "Close Price", title = "Index Constituents' Close Prices", xlabel = "Date", legend = False, datasubset = datasubset_list)
plot1ax(dataset = daily_df_trend, legend = False, ylabel = "Normalised Close Price", title = "Index Constituents' Change in Close Prices", datasubset = datasubset_list, xlabel = "Date")
The graph above shows the change in constituent companies' close prices since COVID.
Saving our data
The cell bellow saves variables to a 'pickle' file to quicken subsequent runs of this code if they are seen as necessary.
# pip install pickle-mixin import pickle pickle_out = open("SPX.pickle","wb") pickl = (COVID_start_date, days_since_COVID, index_Announcement_df, index_Announcement_list, Announcement_COVID_date, Instruments_Income_Statement_Announcement_COVID_date, Instruments_Cash_Flow_Statement_Announcement_COVID_date, Instruments_Balance_Sheet_COVID_date, daily_df, daily_df_no_na, daily_df_trend, datasubset_list) pickle.dump(pickl, pickle_out) pickle_out.close()
The cell bellow can be run to load these variables back into the kernel:
# pickle_in = open("pickl.pickle","rb") # COVID_start_date, days_since_COVID, index_Announcement_df, index_Announcement_list, Announcement_COVID_date, Instruments_Income_Statement_Announcement_COVID_date, Instruments_Cash_Flow_Statement_Announcement_COVID_date, Instruments_Balance_Sheet_COVID_date, daily_df, daily_df_no_na, daily_df_trend, datasubset_list = pickle.load(pickle_in)
Post-COVID-Announcement Price Insight
Now we can start investigating price changes after the first Post-COVID-Announcement of each company in our dataset.
# This is just to delimitate between the code before and after this point daily_df2 = daily_df_no_na
The cell bellow formats the date-type of our data to enable us to apply them to simple algebra.
date_in_date_format =  for k in range(len(daily_df2)): date_in_date_format.append(daily_df2.index[k].date()) daily_df2.index = date_in_date_format
The cell bellow renames the columns of our dataset.
daily_df2_instruments =  for i in daily_df2.columns: daily_df2_instruments.append(str.split(i))
Now: we collect daily prices only for dates after the first Post-COVID-Announcement of each instrument of interest
daily_df2_post_COVID_announcement = pandas.DataFrame() for i,j in zip(daily_df2.columns, daily_df2_instruments): daily_df2_post_COVID_announcement = pandas.merge(daily_df2_post_COVID_announcement, daily_df2[i][daily_df2.index >= Instruments_Income_Statement_Announcement_COVID_date.loc[j].iloc.date()], how = "outer", left_index = True, right_index = True) # Note that the following would not work: ' daily_df2_post_COVID_announcement[i] = daily_df2[i][daily_df2.index >= Instruments_Income_Statement_Announcement_COVID_date.loc[j].iloc.date()] '
Now we can focus on the trend/change in those prices
daily_df2_post_COVID_announcement_trend = pandas.DataFrame() for i in daily_df2.columns: try: daily_df2_post_COVID_announcement_trend = pandas.merge(daily_df2_post_COVID_announcement_trend, daily_df2_post_COVID_announcement.reset_index()[i].dropna().reset_index()[i] - daily_df2_post_COVID_announcement.reset_index()[i].dropna().iloc, how = "outer", left_index = True, right_index = True) except: daily_df2_post_COVID_announcement_trend[i] = numpy.nan
And plot them
plot1ax(dataset = daily_df2_post_COVID_announcement_trend, ylabel = "Normalised Close Price", title = "Index Constituents' Trend In Close Prices From There First Income Statement Announcement Since COVID\n" + "Only companies that announced an Income Statement since the start of COVID (i.e.:" + str(COVID_start_date) + ") will show", xlabel = "Days since first Post-COVID-Announcement", legend = False, # change to "underneath" to see list of all instruments and their respective colors as per this graph's legend. datasubset = datasubset_list)
Some companies have lost and gained a great deal following from their first Post-COVID-Announcement, but most seem to have changed by less than 50 United States of america Dollars (USD).
Post COVID Announcement Price Change
The cell bellow simply gathers all stocks that decreased, increased or did not change in price since their first Post-COVID-Announcement in an easy to digest pandas table. Note that is they haven't had a Post-COVID-Announcement yet, they will show as unchanged.
COVID_priced_in = [,,] for i in daily_df2_post_COVID_announcement_trend.columns: if str(sum(daily_df2_post_COVID_announcement_trend[i].dropna())) != "nan": if numpy.mean(daily_df2_post_COVID_announcement_trend[i].dropna()) < 0: COVID_priced_in.append(str.split(i)) if numpy.mean(daily_df2_post_COVID_announcement_trend[i].dropna()) == 0: COVID_priced_in.append(str.split(i)) if numpy.mean(daily_df2_post_COVID_announcement_trend[i].dropna()) > 0: COVID_priced_in.append(str.split(i)) COVID_priced_in = pandas.DataFrame(COVID_priced_in, index = ["Did not have the negative impact of COVID priced in enough", "Had the effects of COVID priced in (or didn't have time to react to new company announcements)", "Had a price that overcompensated the negative impact of COVID"]) COVID_priced_in
Informative Powers of Announcements Per Sector
We will now investigate the insight behind our analysis per industry sector.
The 2 cells bellow allow us to see the movement in daily price of companies with Post-COVID-Announcements per sector
ESector, err = ek.get_data(instruments = [i.split() for i in daily_df2_post_COVID_announcement_trend.dropna(axis = "columns", how = "all").columns], fields = ["TR.TRBCEconSectorCode", "TR.TRBCBusinessSectorCode", "TR.TRBCIndustryGroupCode", "TR.TRBCIndustryCode", "TR.TRBCActivityCode"]) ESector["TRBC Economic Sector"] = numpy.nan ESector_list = [,,,,,,,,,] Sectors_list = ["Energy", "Basic Materials", "Industrials", "Consumer Cyclicals", "Consumer Non-Cyclicals", "Financials", "Healthcare", "Technology", "Telecommunication Services", "Utilities"] for i in range(len(ESector["TRBC Economic Sector Code"])): for j,k in zip(range(0, 10), Sectors_list): if ESector.iloc[i,1] == (50 + j): ESector.iloc[i,6] = k ESector_list[j].append(ESector.iloc[i,0]) ESector_df = numpy.transpose(pandas.DataFrame(data = [ESector_list[i] for i in range(len(ESector_list))], index = Sectors_list)) ESector_df_by_Sector =  for k in Sectors_list: ESector_df_by_Sector.append(numpy.average([numpy.average(daily_df2_post_COVID_announcement_trend[i + " Close Price"].dropna()) for i in [j for j in ESector_df[k].dropna()]])) ESector_average = pandas.DataFrame(data = ESector_df_by_Sector, columns = ["Average of Close Prices Post COVID Announcement"], index = Sectors_list)
|Average of Close Prices Post COVID Announcement|
The 'ESector_average' table above shows the Close Prices Post COVID-Announcement for each company averaged per sector
The cells bellow now allow us to visualise this trend in a graph on an industry sector basis
Sector_Average =  for k in ESector_average.index: Sector_Average1 =  for j in range(len(pandas.DataFrame([daily_df2_post_COVID_announcement_trend[i + " Close Price"].dropna() for i in ESector_df[k].dropna()]).columns)): Sector_Average1.append(numpy.average(pandas.DataFrame([daily_df2_post_COVID_announcement_trend[i + " Close Price"].dropna() for i in ESector_df[k].dropna()]).iloc[:,j].dropna())) Sector_Average.append(Sector_Average1)
Sector_Average = numpy.transpose(pandas.DataFrame(Sector_Average, index = ESector_average.index))
plot1ax(dataset = Sector_Average, ylabel = "Price Movement", title = "Index Constituents' Trend In Close Prices From There First Income Statement Announcement Since COVID Sorted By Sector\n" + "Only companies that announced an Income Statement since the start of COVID (i.e.:" + str(COVID_start_date) + ") will show", xlabel = "Trading Day", legend = "underneath", datasubset = [i for i in range(len(Sector_Average.columns))])
Using S&P 500 (i.e.: SPX) data, we can have a wholesome picture of industries in the United States of America (USA). We can see a great negative change in instruments’ daily close prices for stocks in the Consumer Cyclical, Utilities, Healthcare and Industrial markets. This is actually surprising because they are the industries that were suggested to be most hindered by COVID in the media before their financial statement announcements; investors thus ought to have priced the negative effects of the Disease on these market sectors appropriately.
The graph suggests that it may be profitable to short companies within these sectors just before they are due to release their first post-COVID Financial Statements - but naturally does not account for future changes, trade costs or other such variants external to this investigation.
Companies in the Financial sector seem to have performed adequately. Reasons for movements in this sector can be complex and numerous due to their exposure to all other sectors.
Tech companies seem to have had the impact of COVID priced in prior to the release of their financial statements. One may postulate the impact of COVID on their share price was actually positive as people rush to online infrastructures they support during confinement.
Companies dealing with Basic Material have performed relatively well. This may be an indication that investors are losing confidence in all but sectors that offer physical goods in supply chains (rather than in consumer goods) - a retreat to fundamentals in a time of uncertainty.
BUT one must use both the ESector_average table and the last graph before coming to any conclusion. The ESector_average - though simple - can provide more depth to our analysis. Take the Healthcare sector for example: One may assume – based on the last graph alone – that this sector is performing badly when revealing information via Announcements; but the ESector_average shows a positive ‘Average of Close Prices Post COVID Announcement’. This is because only very few companies within the Healthcare sector published Announcements before May 2020, and the only ones that did performed badly, skewing the data negatively on the graph.
You can find more detail regarding the Eikon Data API and related technologies for this article from the following resources:
- Refinitiv Eikon Data API page on the Refinitiv Developer Community web site.
- Eikon Data API Quick Start Guide page.
- Eikon Data API Tutorial page.
- Python Quants Video Tutorial Series for Eikon API.
- Eikon Data API Python Reference Guide.
- Eikon Data API Troubleshooting article.
- Pandas API Reference.
For any question related to this example or Eikon Data API, please use the Developers Community Q&A Forum.