Python金融分析(二):金融时间序列数据
前言
金融时间序列数据是金融中最重要的数据类型之一。例如,股票价格随时间的变化代表金融时间序列数据。同样地,欧元/美元汇率随着时间的推移代表了一个金融时间序列; 汇率以短暂的时间间隔报价,然后这些报价的集合是汇率的时间序列。
如果在金融中不考虑时间因素的影响,那结果/结论的可靠性和有效性可能要大打折扣。Python中处理时间序列数据的主要工具是pandas
。实际上,pandas就是其主要的开发者Wes McKinney在大型对冲基金AQR Capital Management担任分析师时开始开发的。几乎可以肯定地说,pandas在设计之初就充分考虑了处理金融时间序列数据。
接下来,我们将分为以下四个部分,利用两个csv文件中的金融时间序列数据作为辅助,初步介绍金融时间序列。
金融数据
准备数据
pandas提供了许多不同的函数和DataFrame方法来导入以不同格式(CSV,SQL,Excel等)存储的数据,并将数据导出为不同的格式。我们用pandas.read_csv()
函数从CSV文件导入时间序列数据集,并查看一下基本信息:
1 | import pandas as pd |
1 | filename = 'source/tr_eikon_eod_data.csv' |
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 2216 entries, 2010-01-01 to 2018-06-29
Data columns (total 12 columns):
AAPL.O 2138 non-null float64
MSFT.O 2138 non-null float64
INTC.O 2138 non-null float64
AMZN.O 2138 non-null float64
GS.N 2138 non-null float64
SPY 2138 non-null float64
.SPX 2138 non-null float64
.VIX 2138 non-null float64
EUR= 2216 non-null float64
XAU= 2211 non-null float64
GDX 2138 non-null float64
GLD 2138 non-null float64
dtypes: float64(12)
memory usage: 225.1 KB
1 | data.plot(figsize=(15,20), subplots=True) |
从图例可能会发现,名字有些奇怪,这是因为我们用的数据是从汤森路透(Thomson Reuters, TR) Eikon Data API获得的,其中用来表示不同金融工具的符号称为路透工具代码(Reuters Instrument Codes, RICs)。
而上面出现的含义为:
1 | instruments = ['Apple Stock', 'Microsoft Stock', |
AAPL.O | Apple Stock
MSFT.O | Microsoft Stock
INTC.O | Intel Stock
AMZN.O | Amazon Stock
GS.N | Goldman Sachs Stock
SPY | SPDR S&P 500 ETF Trust
.SPX | S&P 500 Index
.VIX | VIX Volatility Index
EUR= | EUR/USD Exchange Rate
XAU= | Gold Price
GDX | VanEck Vectors Gold Miners ETF
GLD | SPDR Gold Trust
统计汇总信息
前面我们使用info()
函数查看了一些数据的元信息,我们还可以用describe()
函数查看各列的统计信息:
1 | data.describe().round(2) |
AAPL.O | MSFT.O | INTC.O | AMZN.O | GS.N | SPY | .SPX | .VIX | EUR= | XAU= | GDX | GLD | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 2138.00 | 2138.00 | 2138.00 | 2138.00 | 2138.00 | 2138.00 | 2138.00 | 2138.00 | 2216.00 | 2211.00 | 2138.00 | 2138.00 |
mean | 93.46 | 44.56 | 29.36 | 480.46 | 170.22 | 180.32 | 1802.71 | 17.03 | 1.25 | 1349.01 | 33.57 | 130.09 |
std | 40.55 | 19.53 | 8.17 | 372.31 | 42.48 | 48.19 | 483.34 | 5.88 | 0.11 | 188.75 | 15.17 | 18.78 |
min | 27.44 | 23.01 | 17.66 | 108.61 | 87.70 | 102.20 | 1022.58 | 9.14 | 1.04 | 1051.36 | 12.47 | 100.50 |
25% | 60.29 | 28.57 | 22.51 | 213.60 | 146.61 | 133.99 | 1338.57 | 13.07 | 1.13 | 1221.53 | 22.14 | 117.40 |
50% | 90.55 | 39.66 | 27.33 | 322.06 | 164.43 | 186.32 | 1863.08 | 15.58 | 1.27 | 1292.61 | 25.62 | 124.00 |
75% | 117.24 | 54.37 | 34.71 | 698.85 | 192.13 | 210.99 | 2108.94 | 19.07 | 1.35 | 1428.24 | 48.34 | 139.00 |
max | 193.98 | 102.49 | 57.08 | 1750.08 | 273.38 | 286.58 | 2872.87 | 48.00 | 1.48 | 1898.99 | 66.63 | 184.59 |
也可以用aggregate()
将指定的函数应用到每一列上去:
1 | data.aggregate([min, np.mean, np.std, np.median, max]).round(2) |
AAPL.O | MSFT.O | INTC.O | AMZN.O | GS.N | SPY | .SPX | .VIX | EUR= | XAU= | GDX | GLD | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
min | 27.44 | 23.01 | 17.66 | 108.61 | 87.70 | 102.20 | 1022.58 | 9.14 | 1.04 | 1051.36 | 12.47 | 100.50 |
mean | 93.46 | 44.56 | 29.36 | 480.46 | 170.22 | 180.32 | 1802.71 | 17.03 | 1.25 | 1349.01 | 33.57 | 130.09 |
std | 40.55 | 19.53 | 8.17 | 372.31 | 42.48 | 48.19 | 483.34 | 5.88 | 0.11 | 188.75 | 15.17 | 18.78 |
median | 90.55 | 39.66 | 27.33 | 322.06 | 164.43 | 186.32 | 1863.08 | 15.58 | 1.27 | 1292.61 | 25.62 | 124.00 |
max | 193.98 | 102.49 | 57.08 | 1750.08 | 273.38 | 286.58 | 2872.87 | 48.00 | 1.48 | 1898.99 | 66.63 | 184.59 |
随时间变化趋势
前面的简单分析并没有涉及时间,实际上数据随时间的变化情况可能更加重要,包括绝对差异,百分比变化和对数收益率等。首先,我们可以用pandas.diff()
方法进行差分:
1 | data.diff().head() |
AAPL.O | MSFT.O | INTC.O | AMZN.O | GS.N | SPY | .SPX | .VIX | EUR= | XAU= | GDX | GLD | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Date | ||||||||||||
2010-01-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2010-01-04 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 0.0088 | 23.65 | NaN | NaN |
2010-01-05 | 0.052857 | 0.010 | -0.01 | 0.79 | 3.06 | 0.30 | 3.53 | -0.69 | -0.0043 | -1.35 | 0.46 | -0.10 |
2010-01-06 | -0.487142 | -0.190 | -0.07 | -2.44 | -1.88 | 0.08 | 0.62 | -0.19 | 0.0044 | 19.85 | 1.17 | 1.81 |
2010-01-07 | -0.055714 | -0.318 | -0.20 | -2.25 | 3.41 | 0.48 | 4.55 | -0.10 | -0.0094 | -6.60 | -0.24 | -0.69 |
从统计的角度来看,绝对变化并不一定是最好的选择,因为它们取决于时间序列幅值的影响。 因此,通常优选百分比变化。接下来我们看一下百分比变化(简单收益率):
1 | data.pct_change().round(3).head() |
AAPL.O | MSFT.O | INTC.O | AMZN.O | GS.N | SPY | .SPX | .VIX | EUR= | XAU= | GDX | GLD | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Date | ||||||||||||
2010-01-01 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
2010-01-04 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 0.006 | 0.022 | NaN | NaN |
2010-01-05 | 0.002 | 0.000 | -0.000 | 0.006 | 0.018 | 0.003 | 0.003 | -0.034 | -0.003 | -0.001 | 0.010 | -0.001 |
2010-01-06 | -0.016 | -0.006 | -0.003 | -0.018 | -0.011 | 0.001 | 0.001 | -0.010 | 0.003 | 0.018 | 0.024 | 0.016 |
2010-01-07 | -0.002 | -0.010 | -0.010 | -0.017 | 0.020 | 0.004 | 0.004 | -0.005 | -0.007 | -0.006 | -0.005 | -0.006 |
1 | data.pct_change().mean().plot(kind='bar', figsize=(15,12)) |
除了百分比收益率以外,对数收益率也经常被使用,尤其是对与单一投资品种,考虑对数收益率的可加性,有助于数学建模并具有实际的物理意义:
1 | # Calcuate the log returns in vectorized fashion |
重采样
重采样无论是在金融还是在信号处理中都是重要的操作,例如讲一个原来频率为一天的序列按照周或者月为频率重新采样,获得新的时间序列。
避免后见之明(foresight bias): 在冲采样的时候,pandas.resample()
方法默认采用区间的左端作为重采样的索引。但是在金融分析中,确保使用区间的右端作为索引,并且选择重采样聚合区间的最后一个样本作为重采样的值。否则的话,就可能会导致分析出现预见性偏差。
所谓后见之明偏误指当人们得知某一事件结果后,夸大原先对这一事件的猜测的倾向,也就是俗称的“事后诸葛亮”。后见之明偏见的一个基本的例子是,在知道一个不可预见事件的结果后,一个人相信自己“早就知道结果会这样”。后见之明的偏见可能导致记忆失真,回忆与重建内容时产生错误的理论成果。
1 | # label:划分区间完毕,根据label的不同,区间的索引就不同。 |
滚动统计量(Rolling Statistics)
滚动统计量,实际上也就是以滑动窗口的方法,为窗口内的数据计算某种统计量,从而分析其在局部的结构或者说是短时的特性,例如K线就是一种滚动统计量。我个人感觉,其实并没有太大的意义,但是还是在金融中被广泛使用。
Rolling Statistics基本用法
用pandas计算滚动统计量非常方便,pandas.rolling()
可以满足基本的需求。此外,我们也可以用Cufflinks
中自带的一些滚动统计量,也可以用apply()
函数自定义滚动统计量。我们用苹果公司的股票价格AAPL.O
作为示例:
1 | # define the moving window |
股票技术面分析示例
可能不炒股的朋友都知道,股票交易中有两大“门派”源远流长:基本面分析和技术面分析。前者更加注重宏观政策、行业研究、公司分析、财务报表等,而后者则是通过股票价格的波动情况进行决策。所以,滚动统计量就是技术面分析的主要手段之一。
在技术面分析中,一个非常古老的策略是使用两个 简单滑动平均(simple moving averages, SMAs) ,其背后的思想很简单:当短期的SMA在长期的SMA之上时,应当卖出(沽空);反之亦然。
造成两者负相关的一个原因是:当股票指数下跌时,例如经济危机期间,交易量上升,同时也出现波动; 当股票指数上涨时,投资者通常会保持冷静,并没有看到进行大量交易的动力。 特别是,长期投资者试图进一步推动这一趋势。
所以,当两个SMA交叉的时候也就是切换交易方向的时候。接下来,我们用pandas实现:
1 | # calculate the shorter-term SMA |
1 | apple_stock.dropna(inplace=True) |
从上图可以简单地看出来,大体上就是股票走强一段时间后买入,下跌一段时间后卖出。其实凭感觉也可以判断出来,只是没有这么量化,而另一方面如何设计SMA本身也有主观的成分在内。
相关性分析
当存在多个投资品种的时候,还需要分析不同投资品种间的关联性,可以用于收益的估计和风险管理。但是需要注意的是,对于数据驱动的相关性分析而言,如果没有明确的作用机理,不管是相关系数、线性回归还是格兰杰因果关系,本质上都是相关性而非因果性。当然,如果能够通过控制变量法进行实验的另当别论。
接下来,我们以S&P 500股票指数和VIX波动性指数进行分析,观察到的现象表明两者存在负相关的关系。
S&P 500和VIX数据
1 | sp_vix = data[['.SPX', '.VIX']].dropna() |
将两个时间序列放在一张图中,放大局部,可以更清楚地观察到相关性:
1 | sp_vix.loc[:'2012-12-31'].plot(secondary_y='.VIX', figsize=(15,12)) |
对数收益率
如前所述,在金融中我们通常使用收益率而非幅值的变动情况进行分析。那么,计算两者的对数收益率:
1 | rets = np.log(sp_vix / sp_vix.shift(1)) |
可以看到,两个指数都具有明显的波动聚集性(volatality clusters),接下来我们再看一下两者的散点图,并且可以看一下他们的直方图。
1 | pd.plotting.scatter_matrix(rets, alpha=0.2, hist_kwds={'bins':35}, figsize=(15,12)) |
OLS回归
观察上面的图,两个变量间具有比较明显的相关性,而且两个变量的分布似乎也很接近正态分布。那么,可以考虑用最小二乘法进行回归(有兴趣的可以了解一下Gauss-Markov定理 https://en.wikipedia.org/wiki/Gauss–Markov_theorem )。那么我们用numpy.polyfit()
方法进行拟合:
1 | reg = np.polyfit(rets['.SPX'], rets['.VIX'], deg=1) |
相关性
最后,我们直接看一下相关性度量。考虑这两种情况:一是对于整个数据集计算一个静态的相关性,二是采用滑动窗口的滚动计算相关性。从下图可以看到,两种情况下的相关性都显著小于0,说明具有明显的负相关性。
1 | rets.corr() |
.SPX | .VIX | |
---|---|---|
.SPX | 1.000000 | -0.804382 |
.VIX | -0.804382 | 1.000000 |
1 | ax = rets['.SPX'].rolling(window=252).corr(rets['.VIX']).plot(figsize=(15,12)) |
高频数据
Tick数据是金融时间序列中的特例,但是实际上从信号处理的角度来看,既算不上有多高的频率,处理起来与普通时间序列也有很多相似的地方,反倒是如果从控制论的角度来看,有更多有意思的地方。
使用pandas可以迅速导入具有很多记录的高频数据:
1 | %%time |
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 461357 entries, 2018-06-29 00:00:00.082000 to 2018-06-29 20:59:00.607000
Data columns (total 2 columns):
Bid 461357 non-null float64
Ask 461357 non-null float64
dtypes: float64(2)
memory usage: 10.6 MB
Wall time: 1.17 s
1 | tick['Mid'] = tick.mean(axis=1) |
在使用tick级数据时,经常需要进行降采样。例如,我们将tick级是数据重采样为五分钟的数据,然后可以用来回溯测试算法交易测算或者进行技术分析:
1 | tick_resam = tick.resample(rule='5min', label='right').last() |
小结
本文简要介绍了金融时间序列,并使用pandas进行了初步的分析和数据可视化。接下来我们还会进行更加深入的分析。