用pandas分析事件附近的股票数据

1,234 阅读15分钟

股票收益可能会受到某些事件的严重影响。有时这些事件是出乎意料的或令人惊讶的(自然灾害,全球大流行病,恐怖主义),其他时候它们是预定的(总统选举,收益公告,金融数据发布)。我们可以用pandas来获取金融数据,看看事件对股票收益的影响。

在我之前关于用pandas进行金融市场数据分析的文章中,我研究了获得自由市场数据的基础知识,可视化了SPY交易所交易基金的回报,并研究了季节性的概念,在数据中找到了一些证据。在这篇文章中,我们将在这些知识的基础上,研究股票市场的每日回报,以及它们是如何受到一些重大事件的影响的。

获取数据

首先,让我们抓住我们的历史股票数据。我们将继续看一下SPY ETF。同样,SPY是一种特殊类型的股票,反映了标准普尔500指数的回报。我们将从Alpha Vantage获得我们的数据,就像上次一样。如果你想自己跟随,你应该得到你自己的API密钥,因为我不能给你这个数据。

import pandas as pd
import numpy as np
import os

import matplotlib.pyplot as plt

try:
    API_KEY = open(os.path.expanduser("~/.alphavantage.key"), 'r').readline().strip()
except Exception as ex:
    print("Put your AlphaVantage API key in the file '.alphavantage.key' in your home directory: ", ex)
    API_KEY = "demo"

def get_daily_bars(symbol):
    function = "TIME_SERIES_DAILY"          # daily data
    outputsize = "full"                     # all of it
    datatype = "csv"                        # CSV - comma separated values
    url = f"https://www.alphavantage.co/query?function={function}&symbol=SPY&outputsize=full&apikey={API_KEY}&datatype={datatype}"
    return pd.read_csv(url, parse_dates=['timestamp'], index_col='timestamp').sort_index()

spy_daily = get_daily_bars("SPY")

spy_daily['close'].plot();

SPY stock data

SPY股票数据

关于这个数据需要注意的一点是,它不包括红利。包括红利的数据源是目前Alpha Vantage的高级服务。但对于我们要做的分析来说,这不会是一个主要的问题。

什么影响了股票收益?

看看上面的图表,即使不在X轴上显示日期标签,我们大多数人也能在图表上找出几个点。最明显的是Covid-19大流行病,但你可能也能发现大金融危机和其他更大的动作,如2016年大选。像全球大流行病这样的项目是没有预先计划的,但美国总统选举是有的。

是否有某些事件,我们怀疑对股票回报的影响比其他事件更大?我们可以看看其中的一些,并确定对股票收益的影响大小吗?

美联储

我要欺骗一下,只告诉你一个确实倾向于对美国市场有很大影响的事件:美联储的利率公告。联邦公开市场委员会定期开会,讨论并作出有关利率的决定。这些可能对经济产生巨大的影响,因此大多数金融资产将需要调整其价格,以反映基于这些决定的未来前景。FOMC日可能是高度波动的。让我们看看我们是否能在数据中找到这一点。

上面链接的维基百科文章中,有几个表格包含了关于FOMC决定的信息。事实证明,pandas有能力读取一个html页面,并把它在html中发现的任何表格的列表作为DataFrame 对象反馈给你。让我们看看它是什么样子的。

fomc = pd.read_html("https://en.wikipedia.org/wiki/History_of_Federal_Open_Market_Committee_actions")

print(len(fomc))
fomc[1].head()
5
                 Date Fed. Funds Rate Discount Rate      Votes  \
0   November 05, 2020        0%–0.25%         0.25%       10-0   
1  September 16, 2020        0%–0.25%         0.25%        8-2   
2     August 27, 2020        0%–0.25%         0.25%  unanimous   
3       July 29, 2020        0%–0.25%         0.25%       10-0   
4       June 10, 2020        0%–0.25%         0.25%       10-0   

                                               Notes  Unnamed: 5  
0                                 Official statement         NaN  
1  Kaplan dissented, preferring "the Committee [t...         NaN  
2  No meeting, but announcement of approval of up...         NaN  
3                                 Official statement         NaN  
4                                 Official statement         NaN  

事实证明,这个表是页面中的第二个表。我们可以看到,现在,我们关心的是Date 列。它目前只是文本,让我们把它变成一个数据对象。你可以在这里阅读更多关于pandas数据转换的信息。

fomc_events = fomc[1]
fomc_events['Date'] = pd.to_datetime(fomc_events['Date'])
# we will set the index to be our Data column (explained below)
fomc_events = fomc_events.set_index('Date') # set the Date column as our index
fomc_events.head()
           Fed. Funds Rate Discount Rate      Votes  \
Date                                                  
2020-11-05        0%–0.25%         0.25%       10-0   
2020-09-16        0%–0.25%         0.25%        8-2   
2020-08-27        0%–0.25%         0.25%  unanimous   
2020-07-29        0%–0.25%         0.25%       10-0   
2020-06-10        0%–0.25%         0.25%       10-0   

                                                        Notes  Unnamed: 5  
Date                                                                       
2020-11-05                                 Official statement         NaN  
2020-09-16  Kaplan dissented, preferring "the Committee [t...         NaN  
2020-08-27  No meeting, but announcement of approval of up...         NaN  
2020-07-29                                 Official statement         NaN  
2020-06-10                                 Official statement         NaN  

寻找影响

好了,现在我们有了事件的日期,我们如何用它来匹配这些天的日收益率呢?我可以根据经验告诉你,FOMC会议往往在星期三举行,决定和会议记录通常在美国/纽约14:00发布,这是在正常的股票交易日。然而,其中一些事件可能是紧急会议,可能在市场时间之外发生。

正因为如此,我们希望能够看一下事件发生当天以及未来几天对市场的影响。在某些情况下,市场可能需要一点时间来弄清楚事件的含义以及它将如何影响价格。我们将看看我们是否可以量化这一点。

一种方法是在我们的收益DataFrame ,告诉我们距离上一次FOMC公告已经过去多少天了。有了这些信息,我们就可以比较回报率,看看它们是否有差异。

合并我们的数据

我们有一个密集的日期集(回报)和一个稀疏的日期集(FOMC事件)。我们想找到一种方法,在我们的回报中找出所有发生事件的行,然后向前数到下一个事件。

有很多方法来处理这个问题,但这里是我过去的一个方法。我们首先要做一列,对我们所有的行按顺序进行编号。然后,我们再做第二列,在这一列中,我们将存储从第一列中减去的数值,以形成我们的最终计数器。在有FOMC事件的日子里,我们将把数值设置为该行的数字,并向前填充这些数值,直到下一个事件。如果这听起来令人困惑,没关系,我们将走过这个过程的每一步。

最简单的情况是,我们所有的FOMC事件都可以在我们的回报中找到,我们只需几个步骤就可以做到这一点。

spy_daily['days_since_fomc'] = np.arange(len(spy_daily))
spy_daily['FOMC'] = np.nan # initially put in invalid data so we can fill them later

try:
    # set only the rows with a date
    spy_daily.loc[fomc_events.index, 'FOMC'] = spy_daily.loc[fomc_events.index, 'days_since_fomc']
except Exception as ex:
    print(ex)
"[Timestamp('2020-03-15 00:00:00'), Timestamp('2008-03-16 00:00:00')] not in index"

缺少的日期

似乎有两个日期没有出现在我们的每日股票回报中。事实证明,这些是紧急会议。我可以说我绝对记得这两个事件。这两个都是周日的公告。2020年3月15日,FOMC为应对全球大流行病而进行了第二次降息,并在周日晚上期货市场开盘前几分钟宣布了这个消息。2008年3月16日的事件是由于贝尔斯登公司的倒闭。当时我在雷曼兄弟工作,没有意识到我们也有一些激动人心的事情发生。因为这两天股市没有开盘,我们不能把它们的时间设置为0。那么,我们如何处理这个问题呢?

一种方法是将我们的两个数据集合并。如果我们只想从我们的SPY数据和FOMC表中获得匹配的行,我们可以做一个简单的DataFrame 合并。为了使合并更容易,我通过将Date 列设置为索引,确保这两个DataFrames 具有相同类型的索引。注意,你不必这样做,但它使生活更容易。

spy_daily.merge(fomc_events, left_index=True, right_index=True).head()[['close', 'days_since_fomc']]
               close  days_since_fomc
2000-02-02  141.0625               64
2000-03-21  149.1875               97
2000-05-16  146.6875              136
2000-06-28  145.5625              166
2000-08-22  150.2500              204

修复连接

这可能看起来不错,但是简单的合并的问题是,它将放弃所有与索引不匹配的数据,所以这个合并将不包括我们的紧急会议。这是默认的merge ,可以通过设置how 参数来改变。我们要做的是所谓的外层连接。merge 方法也有一个方便的indicator 参数,它将在你的DataFrame 中放置一列,告诉你产生的行是来自left_onlyright_only ,还是both 。左边和右边是你在代码中从左到右阅读的DataFrames

spy_merged = spy_daily.merge(fomc_events, left_index=True, right_index=True, how='outer', indicator=True)

我们可以使用布尔索引来查看Fed. Funds Rate 不为空的行。这将是FOMC有一个事件的行。我们将在合并后的数据集中找到所有非空值(使用语法~ 来翻转pd.isnull 的结果)。你可以在这里阅读更多关于布尔索引的信息。

spy_merged.loc[~pd.isnull(spy_merged['Fed. Funds Rate']), ['close', 'Fed. Funds Rate', '_merge']].head()
               close Fed. Funds Rate _merge
2000-02-02  141.0625           5.75%   both
2000-03-21  149.1875           6.00%   both
2000-05-16  146.6875           6.50%   both
2000-06-28  145.5625           6.50%   both
2000-08-22  150.2500           6.50%   both

如果我们想看到来自FOMC表的行,我们可以在_merge 列中使用right_only 的值。这些是在我们的FOMC表中的行,不在回报中。

spy_merged.loc[spy_merged['_merge'] == 'right_only', ['close', 'Fed. Funds Rate', '_merge']]
            close Fed. Funds Rate      _merge
2008-03-16    NaN           3.00%  right_only
2020-03-15    NaN        0%–0.25%  right_only

这些是两个'紧急会议'行,在我们最初试图设置上述所有行时被踢出。你可以看到,我们没有这些日期的价格数据(因为市场没有开放)。所以我们将把'FOMC'列设置为这两个日期的下一个日期的行计数器,而把所有其他日期的行计数器设置为该日期行计数器。_merged 找到下一个行的方法是首先shift ,然后用这一列来找到right_only 行。这些将是下一个交易日的行数。

spy_merged.loc[spy_merged['_merge'].shift() == 'right_only', ['close', 'Fed. Funds Rate', '_merge']]
             close Fed. Funds Rate     _merge
2008-03-17  128.30             NaN  left_only
2020-03-16  239.85             NaN  left_only
# set the ones we have in both data sets
spy_merged.loc[spy_merged['_merge'] == 'both', 'FOMC'] = spy_merged.loc[spy_merged['_merge'] == 'both', 'days_since_fomc']
# set the two special meetings
spy_merged.loc[spy_merged['_merge'].shift() == 'right_only', 'FOMC'] = spy_merged.loc[spy_merged['_merge'].shift() == 'right_only', 'days_since_fomc']

现在,我们可以把所有的空数据都填上偏移量。我们使用ffill ,它将用最后观察到的非空值转发填补所有的空值。因此,每次我们有一个FOMC事件,我们将设置从那一天到下一个事件的偏移量为到那一天的总数。

spy_merged['FOMC'] = spy_merged['FOMC'].ffill()

最后一步是从'day_since_fomc'值中减去FOMC的偏移。我将作为一个新的栏目来做,这样你就可以探究一下,看看这两个看起来像什么,但如果我们想的话,我们可以在原地做这个。

spy_merged['days_since_fomc_calced'] = spy_merged['days_since_fomc'] - spy_merged['FOMC']
spy_merged['days_since_fomc_calced'].describe()
count    5525.000000
mean      178.999095
std       270.565997
min         0.000000
25%        14.000000
50%        33.000000
75%       233.000000
max      1128.000000
Name: days_since_fomc_calced, dtype: float64

现在有几件事是显而易见的。首先,我们没有修剪股票数据,以确保我们没有看我们的FOMC来源有数据之前的数据,我们需要修剪到一些合理的大小之后,因为数据来源不是完全最新的。所以我们来做这个。

最简单的方法就是用指数上的布尔选择来限制我们的数据。我们将在我们的数据集中保留最后一个事件后的30天。(如果你想知道为什么我选择在最后做一个copy ,请阅读这个)。

spy_merged = spy_merged.loc[(spy_merged.index >= fomc_events.index[-1])
               & (spy_merged.index <= fomc_events.index[0] + pd.Timedelta(days=30))].copy()

数据看起来也不完整,因为会议之间有一些比我们预期的更大的间隔。我们将在后面处理这个问题,只看我们拥有的公告之后(或之前)的直接数据。

FOMC的影响

哇,这是一个很大的工作,但现在我们有一个数据集,有两个方便的信息。我们有每日的SPY价格(我们可以据此计算回报),还有一栏告诉我们FOMC发表声明后的天数。让我们先看看一些高层次的数据,并添加一个return 列。我们不想看价格,而是看价格一天天的变化。

spy_merged['return'] = spy_merged['close'].pct_change()

现在,我们想了解这些FOMC事件对收益和波动的影响有多大。在这之前,SPY的正常波动率和每日回报率是什么样子的?对于回报率,我在这里会选择看绝对值,因为我对回报率的大小感兴趣,而不是平均回报率。我还将乘以10,000来看看这个基点,这样更容易阅读。基点是百分之一,在金融和交易中,在谈论诸如每日回报时,通常使用基点,因为这些数字往往很小。

spy_merged['return_bps'] = spy_merged['return'] * 10_000
print("Mean return: ", spy_merged['return_bps'].mean())
print("Mean abs return: ", spy_merged['return_bps'].abs().mean())
Mean return:  2.622952572253447
Mean abs return:  81.86473754180587

对于我们的整个数据集,平均每日回报是2.6个基点,但每日回报的平均规模是81个基点(其中一些会上升,一些会下降,但平均而言,它们会上升2.6个基点)。

就波动性而言,我们可以看一下整个集合的标准差,甚至可以按年份看一下。

spy_merged['return_bps'].std()
125.28749957149593

这是整个数据集的标准差。我们也可以按年份来看。你可以看到有些年份的波动性比其他年份要大得多。

spy_merged.groupby(spy_merged.index.year).std()['return_bps']
2000    144.966923
2001    139.365551
2002    166.543256
2003    104.120539
2004     70.723632
2005     65.164303
2006     63.498013
2007     99.968045
2008    259.338830
2009    168.208984
2010    113.180180
2011    144.958668
2012     80.637251
2013     70.117707
2014     71.086169
2015     98.339539
2016     82.738484
2017     42.652961
2018    107.928186
2019     79.060342
2020    218.100669
Name: return_bps, dtype: float64

这也可以通过乘以252的平方根来进行年度化。我们这样做是因为波动率随着时间的平方根而增加。股票一年通常有252个交易日,所以在一年中我们会有252个日波动率的样本。因为我们要看的是年化值,所以我在这里使用百分比回报。

spy_merged.groupby(spy_merged.index.year).std()['return'] * np.sqrt(252) * 100
2000    23.012786
2001    22.123595
2002    26.437922
2003    16.528623
2004    11.227029
2005    10.344512
2006    10.079997
2007    15.869435
2008    41.168763
2009    26.702348
2010    17.966797
2011    23.011475
2012    12.800767
2013    11.130841
2014    11.284580
2015    15.610918
2016    13.134327
2017     6.770948
2018    17.133068
2019    12.550440
2020    34.622408
Name: return, dtype: float64

现在让我们回到我们的FOMC数据,看看FOMC宣布后30天的波动率和回报率是什么样的。这很简单--我们只需寻找事件发生后30天内的所有记录,然后groupby ,按天数计算。

spy_merged_recent = spy_merged.loc[spy_merged['days_since_fomc_calced'] < 30,
                                  ['return_bps', 'days_since_fomc_calced']]

spy_merged_recent.groupby('days_since_fomc_calced').std()['return_bps'].plot(label="return std")
spy_merged_recent.abs().groupby('days_since_fomc_calced').mean()['return_bps'].plot(label="mean abs(return)")
plt.legend();

returns after FOMC dates

FOMC声明后的回报率和波动率。

所以看一下结果,我们可以看到FOMC声明对数据的影响。回报的标准差和规模在声明前后都是最高的,然后稳步下降。对系统有一个冲击,然后它被吸收了。我们可以把这与宣布前的时间进行比较吗?我们可以通过重复我们之前的过程来做到这一点,但要倒过来做。

spy_merged['days_til_fomc'] = np.arange(len(spy_merged), 0, -1)
spy_merged['FOMC'] = np.nan  # reset our counter
# use our earlier results to find the FOMC days
spy_merged.loc[spy_merged['days_since_fomc_calced'] == 0, 'FOMC'] = spy_merged.loc[spy_merged['days_since_fomc_calced'] == 0, 'days_til_fomc']
spy_merged['FOMC'] = spy_merged['FOMC'].bfill() # bfill is backwards fill, same logic
spy_merged['days_til_fomc'] -= spy_merged['FOMC'] # we do this in-place instead this time

spy_merged_before = spy_merged.loc[spy_merged['days_til_fomc'] < 30,
                                  ['return_bps', 'days_til_fomc']]

spy_merged_before.groupby('days_til_fomc').std()['return_bps'].plot(label="return std")
spy_merged_before.abs().groupby('days_til_fomc').mean()['return_bps'].plot(label="mean abs(return)")
plt.legend();

Returns before FOMC dates

FOMC公告前的回报和波动性。

看一下这个数据,我们发现第0天与我们上面的结果相吻合(正如预期)。那是宣布的那一天。但有趣的是,回报的波动性和规模似乎在宣布前在增加。这并不完全是我所期望的。我以为市场会很安静,因为他们在等着听美联储要做什么。但我能想到一个很大的原因,那就是市场不会安静。如果公告是紧急公告,那么美联储极有可能对极端的市场状况做出反应。这些情况肯定会在事件发生前的数据中显示出来。

我现在要把事情总结一下,但这里还有很多可以探讨的地方。我们可以看看其他已知的事件和它们对市场的影响。我们还可以尝试获得更高质量的FOMC事件信息来源。我们显然缺少一些数据,包括一些最近的公告。也许这种探索会促使你更多地去挖掘市场数据。

在pandas的使用方面,我们利用了以下功能。

  • 加载csv数据,包括从一个url中获取数据
  • 从网页上下载表格
  • groupby
  • DataFrame 合并
  • 布尔索引
  • 使用matplotlib进行绘图

你可以随时从github下载这些文章的笔记本。请继续关注后续文章,我将研究其他数据源以及日内收益。