Eevent Study Project

the main objective of this project is to determine whether stock prices react to firm specific news. the event here is the anouncement of revised earnings. the “event.xlsx” provides the dates when a sample of companies announced their revised earnings. “return.xlsx”, contains daily returns for all stocks listed on the Tehran Stock Exchange, along with the daily return of the market index (TEPIX).

Importing the necessary libraries and loading the files

In [1]:
import pandas as pd 
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from persiantools.jdatetime import JalaliDate
%matplotlib inline
In [2]:
# reading the event.xlsx file
eventdf = pd.read_excel(r'C:\Users\kian\Desktop\Assignment 4\Assignment4 files\event.xlsx')
eventdf.head()
Out[2]:
نماد درصد تغيير تاریخ اعلام گزارش
0 آبفر -10.1 1391/09/02
1 آسيا 0 1391/05/04
2 آسيا NaN 1390/01/09
3 آسيا 2.3 1391/11/05
4 آسيا NaN 1388/09/22
In [3]:
eventdf.shape
Out[3]:
(6713, 3)
In [4]:
# reading the return.xlsx file
returndf = pd.read_excel(r'C:\Users\kian\Desktop\Assignment 4\Assignment4 files\return.xlsx')
returndf.head()
Out[4]:
date اخابر آسيا آكنتور البرز باختر بالبر بايكا بترانس بتك ... وليز ومعادن ومعين وملت وملي ونفت ونوين ونيرو ونيكي MR
0 1387/09/17 0.0 NaN 0.0 NaN 0.0 0.0 0.0 -2.150000 0.0 ... -0.210000 0.0 NaN 0.0 0.0 0.0 0.000000 0.0 0.00 -0.005208
1 1387/09/18 0.0 NaN 0.0 NaN 0.0 0.0 0.0 -1.144609 0.0 ... -1.643451 0.0 NaN 0.0 0.0 0.0 -0.070000 0.0 0.00 -0.004523
2 1387/09/20 0.0 NaN 0.0 NaN 0.0 0.0 0.0 -0.465212 0.0 ... -0.621498 0.0 NaN 0.0 0.0 0.0 -0.040028 0.0 -1.48 -0.007206
3 1387/09/23 0.0 NaN 0.0 NaN 0.0 0.0 0.0 -0.186955 0.0 ... -0.492106 0.0 NaN 0.0 0.0 0.0 -0.440485 0.0 0.00 -0.005585
4 1387/09/24 0.0 NaN 0.0 NaN 0.0 0.0 0.0 -2.778356 0.0 ... 0.000000 0.0 NaN 0.0 0.0 0.0 -0.915033 0.0 0.00 -0.007389

5 rows × 392 columns

In [5]:
returndf.shape
Out[5]:
(1547, 392)
In [6]:
# replacing zero values with NaNs in return dataframe
returndf=returndf.replace(0,np.nan)

Part 1

An “event” is described as a positive/negative percentage change in EPS. in this part we remove any missing or zero values from the event data since These are the dates when the companies have kept their previously announced earnings unchanged. the remaining dates would indicate a potential event, which will be analyzed later.

In [7]:
# changing column names from persian to english for ease of use
eventdf.columns = ['symbol','per_change', 'date']
eventdf.head()
Out[7]:
symbol per_change date
0 آبفر -10.1 1391/09/02
1 آسيا 0 1391/05/04
2 آسيا NaN 1390/01/09
3 آسيا 2.3 1391/11/05
4 آسيا NaN 1388/09/22
In [8]:
# removing NAs from the eventdf data frame
eventdf = eventdf.dropna()
eventdf.head()
Out[8]:
symbol per_change date
0 آبفر -10.1 1391/09/02
1 آسيا 0 1391/05/04
3 آسيا 2.3 1391/11/05
6 آسيا 4.5 1389/08/01
7 آسيا -47.3 1390/10/25
In [9]:
type(eventdf.iloc[0,1])
Out[9]:
str
In [10]:
# removing zeros
eventdf = eventdf[eventdf.per_change != '0']
eventdf.head(15)
Out[10]:
symbol per_change date
0 آبفر -10.1 1391/09/02
3 آسيا 2.3 1391/11/05
6 آسيا 4.5 1389/08/01
7 آسيا -47.3 1390/10/25
15 آكنتور 17.8 1388/09/02
23 آكنتور -77/4 1389/12/02
24 آكنتور 15.4 1388/12/05
25 آكنتور -2.8 1391/11/25
30 آكنتور 4.1 1388/05/14
32 اخابر 10.1 1390/12/04
35 اخابر -0.1 1391/12/08
39 اخابر 1 1389/09/12
41 اخابر 7.1 1390/12/02
43 اخابر 46 1389/09/29
44 اخابر 57.9 1388/12/19
In [11]:
# changing "per_change" column data type from string to numeric
# it seems that some numbers are in "/" format rather than "." format. eg: 44/7 instead of 44.7
In [12]:
eventdf['per_change'] = pd.to_numeric(eventdf['per_change'].apply(lambda x: x.replace('/','.')))
eventdf = eventdf.reset_index(drop=True)
eventdf.head(15)
Out[12]:
symbol per_change date
0 آبفر -10.1 1391/09/02
1 آسيا 2.3 1391/11/05
2 آسيا 4.5 1389/08/01
3 آسيا -47.3 1390/10/25
4 آكنتور 17.8 1388/09/02
5 آكنتور -77.4 1389/12/02
6 آكنتور 15.4 1388/12/05
7 آكنتور -2.8 1391/11/25
8 آكنتور 4.1 1388/05/14
9 اخابر 10.1 1390/12/04
10 اخابر -0.1 1391/12/08
11 اخابر 1.0 1389/09/12
12 اخابر 7.1 1390/12/02
13 اخابر 46.0 1389/09/29
14 اخابر 57.9 1388/12/19

Part 2

in this part, we calculate the raw excess return for stock-return data. then we calculate the standard deviation for each stock, based on the raw excess return.

In [13]:
# it seems market return isn't expressed in percentage
returndf['MR'].mean()
Out[13]:
0.0012906204047081714
In [14]:
returndf['MR']=returndf['MR']*100
returndf['MR'].mean()
Out[14]:
0.12906204047081693
In [15]:
# subtracting the market return from the stock returns
returndf = returndf.set_index('date')
ex_returndf = returndf.subtract(returndf['MR'],axis=0)
ex_returndf.head()
Out[15]:
اخابر آسيا آكنتور البرز باختر بالبر بايكا بترانس بتك بسويچ ... وليز ومعادن ومعين وملت وملي ونفت ونوين ونيرو ونيكي MR
date
1387/09/17 NaN NaN NaN NaN NaN NaN NaN -1.629206 NaN 0.330794 ... 0.310794 NaN NaN NaN NaN NaN NaN NaN NaN 0.0
1387/09/18 NaN NaN NaN NaN NaN NaN NaN -0.692279 NaN 1.784862 ... -1.191121 NaN NaN NaN NaN NaN 0.382330 NaN NaN 0.0
1387/09/20 NaN NaN NaN NaN NaN NaN NaN 0.255423 NaN 0.859057 ... 0.099138 NaN NaN NaN NaN NaN 0.680607 NaN -0.759365 0.0
1387/09/23 NaN NaN NaN NaN NaN NaN NaN 0.371574 NaN 1.210188 ... 0.066423 NaN NaN NaN NaN NaN 0.118045 NaN NaN 0.0
1387/09/24 NaN NaN NaN NaN NaN NaN NaN -2.039497 NaN NaN ... NaN NaN NaN NaN NaN NaN -0.176174 NaN NaN 0.0

5 rows × 391 columns

In [16]:
ex_returndf= ex_returndf.drop('MR',axis=1)
ex_returndf.head()
Out[16]:
اخابر آسيا آكنتور البرز باختر بالبر بايكا بترانس بتك بسويچ ... ولغدر وليز ومعادن ومعين وملت وملي ونفت ونوين ونيرو ونيكي
date
1387/09/17 NaN NaN NaN NaN NaN NaN NaN -1.629206 NaN 0.330794 ... 0.380794 0.310794 NaN NaN NaN NaN NaN NaN NaN NaN
1387/09/18 NaN NaN NaN NaN NaN NaN NaN -0.692279 NaN 1.784862 ... NaN -1.191121 NaN NaN NaN NaN NaN 0.382330 NaN NaN
1387/09/20 NaN NaN NaN NaN NaN NaN NaN 0.255423 NaN 0.859057 ... NaN 0.099138 NaN NaN NaN NaN NaN 0.680607 NaN -0.759365
1387/09/23 NaN NaN NaN NaN NaN NaN NaN 0.371574 NaN 1.210188 ... 0.278137 0.066423 NaN NaN NaN NaN NaN 0.118045 NaN NaN
1387/09/24 NaN NaN NaN NaN NaN NaN NaN -2.039497 NaN NaN ... NaN NaN NaN NaN NaN NaN NaN -0.176174 NaN NaN

5 rows × 390 columns

In [17]:
# calculating standaard deviation for each stock 
stock_stds = ex_returndf.std()
stock_stds
Out[17]:
اخابر        2.119378
آسيا         6.958807
آكنتور       3.052863
البرز       15.983064
باختر       11.973482
بالبر        2.416011
بايكا        9.968771
بترانس       2.418589
بتك         10.327631
بسويچ        2.822108
بشهاب        3.795495
بكاب         2.485245
بكام         3.067388
بموتو        3.156560
بنيرو        2.643991
پارتا       11.979414
پارسان      14.975382
پارسيان      7.982240
پاسا        10.355102
پاكشو        1.546850
پتاير        4.657209
پدرخش        4.745456
پدنا              NaN
پرديس        2.894854
پسهند        3.754857
پشاهن       13.738854
پكرمان       5.304955
پكوير      253.517509
پكيان             NaN
پلاست       12.366323
              ...    
وتوسم        2.022586
وتوشه        2.410401
وتوصا        2.240792
وتوكا        2.657747
وخارزم       3.271904
وخاور       17.482015
ورنا         2.695297
وساپا        3.537012
وساخت        2.606688
وسپه         2.267922
وسديد        5.428125
وسكاب        3.549223
وسينا        2.080428
وصنا         1.825336
وصندوق       2.077592
وصنعت        2.241243
وغدير        2.876453
وكار         1.636425
ولساپا       2.487636
ولصنم        2.295607
ولغدر        2.951470
وليز         2.095379
ومعادن       2.058801
ومعين             NaN
وملت         3.621330
وملي         6.065262
ونفت         2.403107
ونوين        2.007540
ونيرو        2.273410
ونيكي        2.382365
Length: 390, dtype: float64

Part 3

To examine market’s reaction to the announcements, we define a window (“event period”) corresponding to each event, consisting of 80 days before the event, the event date itself, and 10 days after the event.

In [18]:
# finding the corresponding gregorian dates for ease of use
eventdf['greg_date']=eventdf['date'].apply(lambda x : JalaliDate(int(x.split('/')[0]),int(x.split('/')[1]),int(x.split('/')[2])).to_gregorian())
In [19]:
eventdf['greg_date']=pd.to_datetime(eventdf['greg_date'])
eventdf.head(10)
Out[19]:
symbol per_change date greg_date
0 آبفر -10.1 1391/09/02 2012-11-22
1 آسيا 2.3 1391/11/05 2013-01-24
2 آسيا 4.5 1389/08/01 2010-10-23
3 آسيا -47.3 1390/10/25 2012-01-15
4 آكنتور 17.8 1388/09/02 2009-11-23
5 آكنتور -77.4 1389/12/02 2011-02-21
6 آكنتور 15.4 1388/12/05 2010-02-24
7 آكنتور -2.8 1391/11/25 2013-02-13
8 آكنتور 4.1 1388/05/14 2009-08-05
9 اخابر 10.1 1390/12/04 2012-02-23
In [20]:
# creating a list of event periods corresponding to each stock
# each list element contains a date range starting from 80 days before the event to 10 days after it
event_periods=[]
for i in eventdf.index:
    event_periods.append(pd.date_range(eventdf['greg_date'][i]-pd.Timedelta('80d'),eventdf['greg_date'][i]+pd.Timedelta('10d'),name = eventdf['symbol'][i]))
In [21]:
len(event_periods[0])
Out[21]:
91

Part 4

The assumption is that all stocks follow a normal distribution, with zero mean and a standard deviation calculated in part 2. For any event period, we check whether the excess return on any day during the event period is significant at the 95% level, and save the results (1 if we have found evidence of significance, and 0 otherwise).

In [22]:
#changing dates to gregorian in return data frame for ease of use
#note: important variables calculated so far: ex_returndf ,eventdf , stock_stds , event_periods
ex_returndf=ex_returndf.reset_index()
In [23]:
ex_returndf['date']=ex_returndf['date'].apply(lambda x : JalaliDate(int(x.split('/')[0]),int(x.split('/')[1]),int(x.split('/')[2])).to_gregorian())
In [24]:
ex_returndf['date']= pd.to_datetime(ex_returndf['date'])
ex_returndf= ex_returndf.set_index('date')
ex_returndf.head()
Out[24]:
اخابر آسيا آكنتور البرز باختر بالبر بايكا بترانس بتك بسويچ ... ولغدر وليز ومعادن ومعين وملت وملي ونفت ونوين ونيرو ونيكي
date
2008-12-07 NaN NaN NaN NaN NaN NaN NaN -1.629206 NaN 0.330794 ... 0.380794 0.310794 NaN NaN NaN NaN NaN NaN NaN NaN
2008-12-08 NaN NaN NaN NaN NaN NaN NaN -0.692279 NaN 1.784862 ... NaN -1.191121 NaN NaN NaN NaN NaN 0.382330 NaN NaN
2008-12-10 NaN NaN NaN NaN NaN NaN NaN 0.255423 NaN 0.859057 ... NaN 0.099138 NaN NaN NaN NaN NaN 0.680607 NaN -0.759365
2008-12-13 NaN NaN NaN NaN NaN NaN NaN 0.371574 NaN 1.210188 ... 0.278137 0.066423 NaN NaN NaN NaN NaN 0.118045 NaN NaN
2008-12-14 NaN NaN NaN NaN NaN NaN NaN -2.039497 NaN NaN ... NaN NaN NaN NaN NaN NaN NaN -0.176174 NaN NaN

5 rows × 390 columns

In [25]:
# since mean=0 , to test for significance we can devide the observation by its standard deviatiom and compare it with t-critical=1.96.
si_tests=[]
for i in range(1,len(event_periods)):
    ob=ex_returndf[event_periods[i].name][event_periods[i]]
    std1=stock_stds[event_periods[i].name]
    t_stat=ob/std1
    si_tests.append((t_stat>1.96) | (t_stat<-1.96))

Part 5

now we can find the number of events in each day of the event window and plot the results.

In [26]:
#creating a dataframe of significance test results for ease of use
si_testsdf=pd.DataFrame()
for i in range(len(si_tests)): 
    si_testsdf = pd.concat([si_testsdf,si_tests[i].rename(si_tests[i].name+str(i)).reset_index(drop=True).to_frame()],axis=1)
In [27]:
# number of events that are significant
no_si_events = si_testsdf.sum(axis=1)
no_si_events
Out[27]:
0       9
1      16
2      12
3      14
4      21
5      17
6      16
7      12
8       8
9      18
10     14
11      6
12      7
13     13
14     10
15     13
16     20
17     13
18     16
19      7
20     13
21     11
22     10
23     19
24     21
25     11
26      9
27     16
28      8
29     17
     ... 
61     15
62     13
63      7
64      7
65     11
66      9
67      6
68     11
69     15
70      5
71     15
72     13
73     13
74     17
75      8
76      5
77      7
78      8
79     13
80     14
81    187
82     99
83     71
84     53
85     43
86     24
87     21
88     13
89      4
90     12
Length: 91, dtype: int64
In [28]:
#------------------------------------- plotting the results --------------------------------------

sns.set_style("darkgrid")
ax = range(-80,11)
plt.figure(figsize=(9, 5))
sns.lineplot(x=ax, y=no_si_events).set(xlabel='days',ylabel='Number of Events')
plt.title('Total Number Of Events For Each Day')
plt.show()

part 6

According to the diagram above, it can be observed that in the first three days after the event, the number of significant excess returns increase drastically. this abnormal behavior shows that the event had a significant effect on the excess return of the stock. on the other hand, in the article about the Mexican market the number of significant excess returns remains constant throughout the event period and shows no abnormal behavior. these observations show that not only were these events value relevant but also there was little to no insider trading inside the Iranian market. in addition, this shows that the market is not informationally inefficient and stock prices are linked to firm values and also the news provided is not anticipated. in other words, none of the scenarios mentioned in the paper for a lack of reaction to firm-specific news announcements are present since in the presence of any of those phenomena there shouldn't have been such a reaction to the event.