我最近看到一些关于讨论隔夜和日内股票收益对比的技术论文的议论。在这篇论文中,我们了解到,隔夜股票的回报远远超过正常交易时间内的日内回报。换句话说,当市场没有开盘时,股票波动最大,但当交易进行时,净收益似乎接近零。该文件称这是一个阴谋,大型对冲基金正在操纵市场。在这篇文章中,我将尝试重现文章中的基本结果,并看看文章中没有讨论的隔夜回报的一个部分。
针对这篇论文,我看到一些人给出了隔夜回报较大的原因,在未来的文章中,我希望能看一下其中的几个原因。但在这第一篇文章中,我只想介绍一些基本情况。
- 使用两个不同的数据来源,重现论文中看到的基本结果
- 如果出现任何不一致的地方,就解决它们
事实证明,涵盖这两个基本步骤将使我们在一篇文章中获得足够的工作机会。在这个过程中,我们将使用两个股票价格数据来源,在这个过程中犯一个错误,了解股票红利,并建立一个矢量的计算。让我们开始吧。
前提
首先,我鼓励你考虑浏览一下这篇论文。很多时候,阅读一篇技术论文可以是一个很好的思路来源,即使该论文有一些缺陷。在这种情况下,作者提供了一些源代码的链接,所以你可以尝试直接复制他的结果。我确实在修改了一些代码后下载并运行了这些代码,将一些代码升级到Python 3。但在这种情况下,我认为从头开始尝试重现数据更容易,因为它是一个简单的计算。
首先,我将从AlphaVantage获取数据,你可以在我之前的 文章中读到更多关于它和如何获取你自己的数据。在这些文章中,我还谈到了一点SPY,即标准普尔500指数交易所交易基金(ETF)。我们将用它作为我们的例子数据。
因此,让我们先获得数据,并绘制SPY的收盘价:
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_daily['close_to_close_return'] = spy_daily['close'].pct_change()
spy_daily['close_to_close_return'].cumsum().plot()
日内回报
该文件谈到了日内和隔夜回报的区别。某一天的日内回报是指从当天交易开始(开盘)到同一天交易结束(收盘)产生的回报。一种方法是用收盘价减去开盘价,再除以开盘价。你也可以重新排列术语,得到一个稍微简单的表达:
# for intraday return, we want the return from open to close
# one way
spy_daily['open_to_close_return'] = (spy_daily['close'] - spy_daily['open'])/spy_daily['open']
spy_daily['open_to_close_return'].cumsum().plot()
# can also do it like this (by just re-arranging terms)
(spy_daily['close']/spy_daily['open'] - 1).cumsum().plot();
隔夜收益
现在,隔夜回报是由一天的收盘价到第二天的开盘价的变化产生的。为了得到前一天的收盘价,我们使用Series.shift()
方法将其移动一天。这将收盘价向前移动了一天,与开盘价保持一致,使我们能够看到总的隔夜收益:
# overnight (close to open) returns
spy_daily['close_to_open_return'] = (spy_daily['open']/spy_daily['close'].shift() - 1).shift()
spy_daily['close_to_open_return'].cumsum().plot()
现在,让我们把所有这些都放在一个图上:
# put them all together
spy_daily['close_to_close_return'].cumsum().plot(label="Close to Close")
spy_daily['close_to_open_return'].cumsum().plot(label="Close to Open (overnight)")
spy_daily['open_to_close_return'].cumsum().plot(label="Open to Close (intraday)")
plt.legend()
成功吗?
这看起来就像我们开始看到论文中建议的那样的图。我们可以看到SPY盘中的总回报接近于0,甚至在数据集的大部分时间里是负数。隔夜回报有时甚至更糟,但自2009年以来,隔夜回报要高得多。有趣的是。
另一个数据来源
现在,我们不使用AlphaVantage,而是使用另一个免费的数据源,雅虎财经。这篇论文的作者使用雅虎作为论文中所有计算的来源,所以让我们看看那个数据是什么样子。
我通常使用一个很好的Python库来访问雅虎,它使代码稍微容易阅读,并以pandas类型返回数据。它还为我们提供了一个很好的方法来抓取其他一些有趣的数据,比如分叉和分红。你可以用pip install yfinance
来安装它。
我将只是重现结果,但有第二个数据集:
# author used Yahoo! finance for data, so let's do that as well
import yfinance as yf
spy_yf = yf.Ticker("SPY")
spy_yf_hist = spy_yf.history(period="max")
# calc them
spy_yf_hist['close_to_close_return'] = spy_yf_hist['Close'].pct_change()
spy_yf_hist['close_to_open_return'] = spy_yf_hist['Open']/spy_yf_hist['Close'].shift() - 1
spy_yf_hist['open_to_close_return'] = spy_yf_hist['Close']/spy_yf_hist['Open'] - 1
# plot them
spy_yf_hist['close_to_close_return'].cumsum().plot(label="Close to Close")
spy_yf_hist['close_to_open_return'].cumsum().plot(label="Close to Open (overnight)")
spy_yf_hist['open_to_close_return'].cumsum().plot(label="Open to Close (intraday)")
plt.legend()
数据差异
首先,我们可以看到,这看起来比我们在AlphaVantage中看到的要戏剧性得多,因为数据一直可以追溯到1992年。隔夜回报是总回报的大部分,日内回报甚至在很多时候都是负的。使用雅虎数据的一个好处是,我们至少看到了论文作者看到的东西。让我们把AlphaVantage的数据与雅虎的数据进行比较。
注:我把雅虎的数据切断,使之与AlphaVantage的起始日期相同。如果你有关于索引时间序列数据的问题,你可以查看这篇文章。
我还做了一份数据的副本。好奇我为什么这样做?那么这篇文章可能会让你感兴趣:
# we make a copy so we can modify it later
spy_yf_short = spy_yf_hist[spy_daily.index[0]:].copy()
for c in ['close_to_close_return', 'open_to_close_return', 'close_to_open_return']:
spy_daily[c].cumsum().plot(label=f"{c} (AV)")
spy_yf_short[c].cumsum().plot(label=f"{c} (Yahoo!)")
plt.legend()
我们可以看到,这两个回报并不匹配。雅虎的回报率比AlphaVantage的高。想知道为什么吗?
公司行动
每当你在看股票价格数据时,重要的是要了解价格是否包括拆分和分红,这有时也被称为公司行动。
最简单的例子是一个简单的股票分割。如果股票的价格很高,董事会可能会决定将股票分割成更小的规模。因此,一股$100
的股票将变成10股$10
的股票。如果你只看价格,似乎已经下降到1/10的数量。
作为另一个例子,你可能拥有一家公司的股票,而董事会决定将他们的一个业务部门分拆为一个独立的公司。董事会可以决定,每个股东每拥有一份旧公司的股票,就可以得到一份新公司的股票,但是股票的价格会在拆分当天发生变化以反映这一点。例如,$20
的股票可能会变成$18
的股票,而新的$2
的股票会给股东。如果你只看交易时的价格,看起来那天会有额外的损失$2
。实际上,股东收到的是新公司的股份,而且该股份将在未来单独交易。这有点过于简化了,但关键是你不要在数据中忽视这一点。
就股息而言,只要股东在有资格获得股息的最后一天,即除息日,拥有股票,就能从公司获得股息付款。这通常是在记录股息的前两天。
现在我们为什么要关心这个问题?嗯,首先,对于一些股票来说,这实际上可以增加相当多的钱。而且,由于SPY是一个交易所交易基金,它持有一些确实支付股息的股票,它也支付股息,这是ETF中的股票的股息之和。
请注意,AlphaVantage对包括价格历史中的股息的高级服务收取额外费用。你在上述数据中得到的价格是 "交易时 "的价格。换句话说,这个价格与你在历史上看到的那一天的价格完全一样,而红利都没有。另一方面,雅虎给我们的是包含股息的价格。
现在我们知道了这一点,上面的图就有意义了,因为只有一个图包括了股息。让我们把它们加起来。如果我们把回报相加,目前的价格中有多少是红利?
spy_yf_hist['Dividends'].cumsum().plot()
spy_yf_hist['Close'].plot()
print((spy_yf_hist['Dividends'].cumsum()/spy_yf_hist['Close'])[-1])
print(spy_yf_hist['Dividends'].sum())
0.19159901587041897
80.485
隔夜回报的一个来源--红利
在我们1992年以来的数据中,有80美元的红利支付,约占价格的20%(截至运行源笔记本的时间)。
首先,有多少日期有红利?让我们通过找到有非零股息的行来计算。如果你不确定索引是如何工作的,请查看关于索引的系列文章中的第一篇。在这种情况下,我创建一个布尔索引,选择非0的行:
spy_yf_hist.loc[spy_yf_hist['Dividends'] != 0].shape
(117, 10)
历史上有117次派发的股息。传统上,股票会每季度支付一次股息。让我们来看看AlphaVantage(无红利)和雅虎(有红利)数据中红利发放时的数据。注意,我看的是完整的数据集来得到这个日期。
spy_yf_hist.loc['2021-12-16':'2021-12-20', ['Open', 'Close', 'Dividends', 'close_to_open_return']]
Open Close Dividends close_to_open_return
Date
2021-12-16 470.915555 464.816986 0.000 0.004186
2021-12-17 461.549988 459.869995 1.633 -0.007029
2021-12-20 454.480011 454.980011 0.000 -0.011721
spy_daily.loc['2021-12-16':'2021-12-20', ['open', 'close', 'close_to_open_return']]
open close close_to_open_return
timestamp
2021-12-16 472.57 466.45 0.000129
2021-12-17 461.55 459.87 0.004186
2021-12-20 454.48 454.98 -0.010505
看一下这些数据,我们似乎可以看到股息是如何应用的。对于雅虎的数据(最上面的那个),价格似乎都被历史上的股息数额所调整。对于派发股息的日子,你可以看到雅虎的回报高于Alpha Vantage的回报,因为它包括了股息的回报。SPY只是一堆股票放在一起,所以它支付的股息与单个股息的总和相匹配(大致如此)。那么,有多少回报是单单由于分红而产生的?
spy_daily['close_to_open_return'].cumsum().plot(label="Close to Close (AV)")
spy_yf_short['close_to_open_return'].cumsum().plot(label="Close to Close (Y!)")
plt.legend()
without_dividends = spy_daily['close_to_open_return'].sum()
with_dividends = spy_yf_short['close_to_open_return'].sum()
print(f"with: {with_dividends * 100:.2f}%, without: {without_dividends* 100:.2f}, difference: {(with_dividends - without_dividends)*100:.2f}%")
with: 173.23%, without: 134.50, difference: 38.73%
因此,SPY的隔夜回报中大约有20%是单独来自股息。显然,隔夜和日内回报之间仍有鲜明的对比,我们需要继续思考这个问题。但红利是一个很大的贡献因素。在报纸上进行文字搜索,甚至没有提到它们。
仔细检查结果
确保你有正确答案的一个方法是重复检查你的结果。我喜欢做的一个方法是,看看我是否可以将一个数据集的数据转化为第二个数据集的数据。让我们使用雅虎股息数据,并尝试使用AlphaVantage的价格得出调整后的价格。首先,让我们使用雅虎的API来获取股息数据。我们将忽略一个事实,即我们已经在价格数据中拥有它。事实证明,这是微不足道的:
spy_yf.dividends
Date
1993-03-19 0.213
1993-06-18 0.318
1993-09-17 0.286
1993-12-17 0.317
1994-03-18 0.271
...
2020-12-18 1.580
2021-03-19 1.278
2021-06-18 1.376
2021-09-17 1.428
2021-12-17 1.633
Name: Dividends, Length: 117, dtype: float64
现在,为了计算去除红利后的价格,我们应该只需要去除历史价格中的红利部分。这样做的效果是使我们的回报率更高。要做到这一点,我们首先要在我们的DataFrame
,添加一个红利列。由于这个数据很稀疏(记得每年只有4次付款),大部分行中都会有NaN
s。
spy_daily['dividend'] = spy_yf.dividends
为了抵消价格,我们将做三件事:
- 将它们相加
- 将它们与正确的日期对齐
- 从价格中减去股息
关于总和的一个注意事项,它需要通过时间向后增加,因为你越往后走,从价格中去除的红利就越多。记住,在你的价格数据中包含股息,必须使它看起来像你在过去以较低的价格买入股票,所以它现在更有价值。这是因为实际价格在支付股息时被交易所本身的股息金额降低了。
我做了一份数据的副本,看看包括股息回报的数据会是什么样子。请注意,下面的代码有许多链式方法。我不是一下子就打出来的,我不得不尝试了几次反复,才想出了正确的组合。但这展示了你如何在pandas中使用连锁方法来转换你的数据。如果你不了解这些方法,我鼓励你一个一个地去看,看看如何转换数据。
下面是我们所做的,一步步来:
fillna
用 "0 "来填充空值sort_index(ascending=False)
按逆时针顺序对数值进行排序cumsum
将红利做一个累积的总和sort_index
将指数按时间顺序重新排序shift(-1)
将数值向后移动一天,以便在其生效日期进行。
spy_daily_r = spy_daily.copy()
spy_daily_r['total_dividend'] = spy_daily_r['dividend'].fillna(0).sort_index(ascending=False).cumsum().sort_index().shift(-1)
spy_daily_r.loc['2021-12-16':'2021-12-20', ['open', 'close', 'close_to_open_return', 'total_dividend']]
open close close_to_open_return total_dividend
timestamp
2021-12-16 472.57 466.45 0.000129 1.633
2021-12-17 461.55 459.87 0.004186 0.000
2021-12-20 454.48 454.98 -0.010505 0.000
现在,我们将更新我们的价格以去除总红利。我只是通过我们的四个价格(最高价、最低价、开盘价、收盘价),把总红利退掉,然后绘制结果:
for c in ['high', 'low', 'open', 'close']:
spy_daily_r[c] = spy_daily_r[c] - spy_daily_r['total_dividend']
spy_yf_short['Close'].plot(label="Yahoo")
spy_daily['close'].plot(label="AlphaVantage")
spy_daily_r['close'].plot(label="AlphaVantage (with dividends applied)")
plt.legend()
实际计算红利
哦,不,它没有工作!应用于AlphaVantage数据的红利与雅虎的数据不匹配。 会是什么问题呢?在做了一些调查后,我意识到我天真的从价格中退去红利的方法并不是正确的方法。考虑下面的例子:假设一只股票的价格是$10
,每季度支付$0.25
股息。价格在15年内保持在$10
。调整后的价格将是多少?如果我们只从当前价格中减去累计股息,我们最终会得到一个负的价格,即$-5
,或$10-(4x$0.25)*15
。要使价格保持在0以上,我们将需要另一种方法。
事实证明,雅虎的计算方法是使用了证券价格研究中心(CRSP)的一种标准化技术,这里的解释有点不到位。不是从价格中减去总的红利价值,而是根据红利和红利发放前的最近收盘价计算出一个乘数。这个系数总是大于0,小于1,将被应用于所有的价格,直到下一次分红的时间追溯。在下一次分红时,会计算一个新的系数。
这个系数总是计算为1 - (dividend/price)
,所有的系数都是向后相乘的,然后应用于所有的价格。请注意,累积因子每季度只改变一次。
为了在pandas中应用这种技术,我们只需要为每一行获得正确的价格和红利。价格将是分红前一天的收盘价。
注意,程序员可能会想写一个函数,遍历DataFrame
中的所有行来计算这个值。如果你有典型的Python编程经验,这对你来说可能是非常直接的。但是一个更好的方法是创建一个矢量的解决方案。关于为什么这样做更好,请看这篇文章。
首先,我们需要找到分红前一天的收盘价。像先前一样,我们使用一个布尔指数,但我们把它往后移一天:
# shift the dividends back 1 day, use them to obtain the rows with the prices to use
spy_daily_d = spy_daily.copy()
spy_daily_d.loc[~pd.isnull(spy_daily_d['dividend'].shift(-1)), 'close']
timestamp
1999-12-16 142.1250
2000-03-16 146.3437
2000-06-15 148.1562
2000-09-14 149.6406
2000-12-14 134.4062
...
2020-12-17 372.2400
2021-03-18 391.4800
2021-06-17 421.9700
2021-09-16 447.1700
2021-12-16 466.4500
Name: close, Length: 90, dtype: float64
注意,这些价格有一个索引值(一个日期),是这个价格开始往后使用的日期。我们现在只需要计算我们的系数,用每个价格的红利来计算。我们使用与上面相同的概念抓取红利:
spy_daily_d.loc[~pd.isnull(spy_daily_d['dividend']), 'dividend']
timestamp
1999-12-17 0.348
2000-03-17 0.371
2000-06-16 0.348
2000-09-15 0.375
2000-12-15 0.411
...
2020-12-18 1.580
2021-03-19 1.278
2021-06-18 1.376
2021-09-17 1.428
2021-12-17 1.633
Name: dividend, Length: 90, dtype: float64
注意,这里的指数偏离了一天,这是股息本身的日期。我们现在可以计算因子了,但是我们必须注意我们的指数我们希望使用来自价格的指数,而不是来自股息的指数,所以我们只是从股息中抓取values
。因子已经准备好了,可以一次性计算:
factor = 1 - (spy_daily_d.loc[~pd.isnull(spy_daily_d['dividend']), 'dividend'].values/
spy_daily_d.loc[~pd.isnull(spy_daily_d['dividend'].shift(-1)), 'close'])
factor
timestamp
1999-12-16 0.997551
2000-03-16 0.997465
2000-06-15 0.997651
2000-09-14 0.997494
2000-12-14 0.996942
...
2020-12-17 0.995755
2021-03-18 0.996735
2021-06-17 0.996739
2021-09-16 0.996807
2021-12-16 0.996499
Name: close, Length: 90, dtype: float64
你可以在这里看到,我们现在有一系列的因素和它们生效的日期。我们的最后一步是通过将这些因子相乘来回溯应用。你可以在pandas中使用cumprod
,它的工作原理与cumsum
相似,但它不是求和,而是求积。同样,我们需要做我们在错误的解决方案中所做的sort_index
,以使其在时间上向后而不是向前:
factor.sort_index(ascending=False).cumprod().sort_index()
timestamp
1999-12-16 0.664324
2000-03-16 0.665955
2000-06-15 0.667648
2000-09-14 0.669219
2000-12-14 0.670901
...
2020-12-17 0.982657
2021-03-18 0.986846
2021-06-17 0.990078
2021-09-16 0.993317
2021-12-16 0.996499
Name: close, Length: 90, dtype: float64
只是抽查一下,这是有意义的。我们越往后走,系数应该越小,使过去的价格更低,所以由于红利的影响,我们的回报更高。最后,让我们把它应用于我们的价格,看看我们是否能与雅虎的数据相匹配:
# second attempt. Make a new copy of the original source data that is pristine
spy_daily_r2 = spy_daily.copy()
spy_daily_r2['factor'] = factor.sort_index(ascending=False).cumprod().sort_index()
# backfill the missing values with the most recent value (i.e the factor from that dividend)
spy_daily_r2['factor'].bfill(inplace=True)
spy_daily_r2['factor'].fillna(1, inplace=True) # base case
for c in ['high', 'low', 'open', 'close']:
spy_daily_r2[c] = spy_daily_r2[c] * spy_daily_r2['factor']
spy_yf_short['Close'].plot(label="Yahoo")
spy_daily['close'].plot(label="AlphaVantage")
spy_daily_r2['close'].plot(label="AlphaVantage (with dividends applied)")
plt.legend()
现在我们有了一个匹配的结果!我承认,我花了不少时间来调试,才把它弄好。这里有一些关于如何不犯我在弄清这个问题时所犯的同样错误的提示。
不要重复使用你修改过的数据
如果你从一个原始来源(在我的例子中是雅虎和AlphaVantage)获取了数据,然后在一次失败的尝试中对其进行了修改,那么就不要在另一次尝试中使用这个DataFrame
,从头开始。在我的例子中,我重新使用了我的数据,从我纠正后的实施中的不正确的方法倒出红利,我仍然是错误的(因为我的价格已经被修改了)。确保你能从头到尾拉出数据,并知道所有的修改。做到这一点的一个方法是把你的笔记本分成独立的笔记本,用于每次尝试。
好好利用可视化的错误
如果你有两个应该匹配的数据集,绘制它们的比率,看看它们有多接近对方。如果它们不是真的接近于1,你就知道你有问题了。然后寻找它们分歧的日期。对我来说,我注意到这些分歧发生在分红日期,所以我意识到我在某一点上破坏了数据。
考虑一下matplotlib notebook
默认情况下,matplotlib的图在Jupyter中是内联显示的。在Jupyter中,有一个特殊的魔法,可以在你的浏览器中生成交互式的图,%matplotlib notebook
。把这个放在你绘制数据的笔记本单元格的顶部,你想与之互动。你可以放大差异。
详细查看个别行
我对单行进行了手工计算,直到我确信正确的数值应该是什么。一旦我有了一个知道的好结果,我就可以看那些行来仔细检查我的计算结果。如果你觉得有帮助的话,你甚至可以把这个放在电子表格里。
好了,我们已经谈得够多了。希望在这一点上,你对红利的了解比你开始时更多,而且不应忽视它们!这就是红利。
还有什么原因会导致隔夜收益主导日内收益呢?我将在以后的文章中再看一些潜在的原因。那些文章将包括下载、清理和分析数据的代码和技术,就像这篇文章一样。