Python金融分析(二):金融时间序列数据

前言

金融时间序列数据是金融中最重要的数据类型之一。例如,股票价格随时间的变化代表金融时间序列数据。同样地,欧元/美元汇率随着时间的推移代表了一个金融时间序列; 汇率以短暂的时间间隔报价,然后这些报价的集合是汇率的时间序列。

如果在金融中不考虑时间因素的影响,那结果/结论的可靠性和有效性可能要大打折扣。Python中处理时间序列数据的主要工具是pandas。实际上,pandas就是其主要的开发者Wes McKinney在大型对冲基金AQR Capital Management担任分析师时开始开发的。几乎可以肯定地说,pandas在设计之初就充分考虑了处理金融时间序列数据。

接下来,我们将分为以下四个部分,利用两个csv文件中的金融时间序列数据作为辅助,初步介绍金融时间序列。

金融数据

准备数据

pandas提供了许多不同的函数和DataFrame方法来导入以不同格式(CSV,SQL,Excel等)存储的数据,并将数据导出为不同的格式。我们用pandas.read_csv()函数从CSV文件导入时间序列数据集,并查看一下基本信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
import pandas as pd
import altair as alt
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt

%matplotlib inline

# set figure size
mpl.rcParams['figure.figsize']=[15,12]

# set figure style
plt.style.use('ggplot')
1
2
3
4
filename = 'source/tr_eikon_eod_data.csv'
# Specifies that the index values are of type datetime.
data = pd.read_csv(filename, index_col=0, parse_dates=True)
data.info()
<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
2
data.plot(figsize=(15,20), subplots=True)
plt.show()

output_7_0.png

从图例可能会发现,名字有些奇怪,这是因为我们用的数据是从汤森路透(Thomson Reuters, TR) Eikon Data API获得的,其中用来表示不同金融工具的符号称为路透工具代码(Reuters Instrument Codes, RICs)。
而上面出现的含义为:

1
2
3
4
5
6
7
8
9
instruments = ['Apple Stock', 'Microsoft Stock',
'Intel Stock', 'Amazon Stock', 'Goldman Sachs Stock',
'SPDR S&P 500 ETF Trust', 'S&P 500 Index',
'VIX Volatility Index', 'EUR/USD Exchange Rate',
'Gold Price', 'VanEck Vectors Gold Miners ETF',
'SPDR Gold Trust']

for ric, name in zip(data.columns, instruments):
print('{:8s} | {}'.format(ric, name))
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))

output_20_1.png

除了百分比收益率以外,对数收益率也经常被使用,尤其是对与单一投资品种,考虑对数收益率的可加性,有助于数学建模并具有实际的物理意义:

1
2
3
4
5
# Calcuate the log returns in vectorized fashion
rets = np.log(data/data.shift(1))
# Plot the cumulative log returns over time
returns = rets.cumsum().apply(np.exp)
returns.plot(figsize=(15,12))

output_22_1.png

重采样

重采样无论是在金融还是在信号处理中都是重要的操作,例如讲一个原来频率为一天的序列按照周或者月为频率重新采样,获得新的时间序列。

避免后见之明(foresight bias): 在冲采样的时候,pandas.resample()方法默认采用区间的左端作为重采样的索引。但是在金融分析中,确保使用区间的右端作为索引,并且选择重采样聚合区间的最后一个样本作为重采样的值。否则的话,就可能会导致分析出现预见性偏差。

所谓后见之明偏误指当人们得知某一事件结果后,夸大原先对这一事件的猜测的倾向,也就是俗称的“事后诸葛亮”。后见之明偏见的一个基本的例子是,在知道一个不可预见事件的结果后,一个人相信自己“早就知道结果会这样”。后见之明的偏见可能导致记忆失真,回忆与重建内容时产生错误的理论成果。

1
2
3
4
5
6
# label:划分区间完毕,根据label的不同,区间的索引就不同。
# 如果label为left,则区间左边的日期作为索引;
# 如果label为right,则区间右边的日期作为索引。

# pandas的resample()方法可以视作按照重新采样的区间进行聚合
returns.resample('1m', label='right').last().plot(figsize=(15,12))

output_25_1.png

滚动统计量(Rolling Statistics)

滚动统计量,实际上也就是以滑动窗口的方法,为窗口内的数据计算某种统计量,从而分析其在局部的结构或者说是短时的特性,例如K线就是一种滚动统计量。我个人感觉,其实并没有太大的意义,但是还是在金融中被广泛使用。

Rolling Statistics基本用法

用pandas计算滚动统计量非常方便,pandas.rolling()可以满足基本的需求。此外,我们也可以用Cufflinks中自带的一些滚动统计量,也可以用apply()函数自定义滚动统计量。我们用苹果公司的股票价格AAPL.O作为示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# define the moving window
window = 20
apple_stock = pd.DataFrame(data['AAPL.O']).dropna()

# calculate the rolling mean value
apple_stock['mean'] = apple_stock['AAPL.O'].rolling(window=window).mean()

apple_stock['min'] = apple_stock['AAPL.O'].rolling(window=window).min()

apple_stock['max'] = apple_stock['AAPL.O'].rolling(window=window).max()

# calcuate the exponentially weighted moving average
# with decay in terms of a half life of 0.5
apple_stock['ewma'] = apple_stock['AAPL.O'].ewm(halflife=0.5, min_periods=window).mean()

ax = apple_stock[['min', 'mean', 'max']][-200:].plot(
figsize=(15,12), style=['g--', 'r--', 'g--'])
apple_stock['AAPL.O'][-200:].plot(ax=ax, legend='AAPL.O', color='blue', lw=2)

output_30_1.png

股票技术面分析示例

可能不炒股的朋友都知道,股票交易中有两大“门派”源远流长:基本面分析和技术面分析。前者更加注重宏观政策、行业研究、公司分析、财务报表等,而后者则是通过股票价格的波动情况进行决策。所以,滚动统计量就是技术面分析的主要手段之一。

在技术面分析中,一个非常古老的策略是使用两个 简单滑动平均(simple moving averages, SMAs) ,其背后的思想很简单:当短期的SMA在长期的SMA之上时,应当卖出(沽空);反之亦然。

造成两者负相关的一个原因是:当股票指数下跌时,例如经济危机期间,交易量上升,同时也出现波动; 当股票指数上涨时,投资者通常会保持冷静,并没有看到进行大量交易的动力。 特别是,长期投资者试图进一步推动这一趋势。

所以,当两个SMA交叉的时候也就是切换交易方向的时候。接下来,我们用pandas实现:

1
2
3
4
# calculate the shorter-term SMA
apple_stock['SMA1'] = apple_stock['AAPL.O'].rolling(window=42).mean()
# calculate the longer-term SMA
apple_stock['SMA2'] = apple_stock['AAPL.O'].rolling(window=252).mean()
1
2
3
4
5
6
7
apple_stock.dropna(inplace=True)

apple_stock['positions'] = np.where(apple_stock['SMA1'] > apple_stock['SMA2'], 1, -1)

ax = apple_stock[['AAPL.O', 'SMA1', 'SMA2', 'positions']].plot(
figsize=(15,12), lw=2, secondary_y='positions')
ax.get_legend().set_bbox_to_anchor((0.15, 0.9))

output_34_0.png

从上图可以简单地看出来,大体上就是股票走强一段时间后买入,下跌一段时间后卖出。其实凭感觉也可以判断出来,只是没有这么量化,而另一方面如何设计SMA本身也有主观的成分在内。

相关性分析

当存在多个投资品种的时候,还需要分析不同投资品种间的关联性,可以用于收益的估计和风险管理。但是需要注意的是,对于数据驱动的相关性分析而言,如果没有明确的作用机理,不管是相关系数、线性回归还是格兰杰因果关系,本质上都是相关性而非因果性。当然,如果能够通过控制变量法进行实验的另当别论。

接下来,我们以S&P 500股票指数和VIX波动性指数进行分析,观察到的现象表明两者存在负相关的关系。

S&P 500和VIX数据

1
2
sp_vix = data[['.SPX', '.VIX']].dropna()
sp_vix.plot(subplots=True, figsize=(15,12))

output_39_1.png

将两个时间序列放在一张图中,放大局部,可以更清楚地观察到相关性:

1
sp_vix.loc[:'2012-12-31'].plot(secondary_y='.VIX', figsize=(15,12))

output_41_1.png

对数收益率

如前所述,在金融中我们通常使用收益率而非幅值的变动情况进行分析。那么,计算两者的对数收益率:

1
2
3
rets = np.log(sp_vix / sp_vix.shift(1))
rets.dropna(inplace=True)
rets.plot(subplots=True, figsize=(15,12))

output_44_1.png

可以看到,两个指数都具有明显的波动聚集性(volatality clusters),接下来我们再看一下两者的散点图,并且可以看一下他们的直方图。

1
pd.plotting.scatter_matrix(rets, alpha=0.2, hist_kwds={'bins':35}, figsize=(15,12))

output_46_1.png

OLS回归

观察上面的图,两个变量间具有比较明显的相关性,而且两个变量的分布似乎也很接近正态分布。那么,可以考虑用最小二乘法进行回归(有兴趣的可以了解一下Gauss-Markov定理 https://en.wikipedia.org/wiki/Gauss–Markov_theorem )。那么我们用numpy.polyfit()方法进行拟合:

1
2
3
reg = np.polyfit(rets['.SPX'], rets['.VIX'], deg=1)
ax = rets.plot(kind='scatter', x='.SPX', y='.VIX', figsize=(15,12))
ax.plot(rets['.SPX'], np.polyval(reg, rets['.SPX']), 'r', lw=2)

output_49_1.png

相关性

最后,我们直接看一下相关性度量。考虑这两种情况:一是对于整个数据集计算一个静态的相关性,二是采用滑动窗口的滚动计算相关性。从下图可以看到,两种情况下的相关性都显著小于0,说明具有明显的负相关性。

1
rets.corr()
.SPX .VIX
.SPX 1.000000 -0.804382
.VIX -0.804382 1.000000
1
2
ax = rets['.SPX'].rolling(window=252).corr(rets['.VIX']).plot(figsize=(15,12))
ax.axhline(rets.corr().iloc[0, 1], c='b')

output_53_1.png

高频数据

Tick数据是金融时间序列中的特例,但是实际上从信号处理的角度来看,既算不上有多高的频率,处理起来与普通时间序列也有很多相似的地方,反倒是如果从控制论的角度来看,有更多有意思的地方。

使用pandas可以迅速导入具有很多记录的高频数据:

1
2
3
4
5
%%time
# data from FXCM Forex Capital Markets Ltd.
tick = pd.read_csv(r'source/fxcm_eur_usd_tick_data.csv',
index_col=0, parse_dates=True)
tick.info()
<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
2
tick['Mid'] = tick.mean(axis=1)
tick['Mid'].plot(figsize=(15,12))

output_57_1.png

在使用tick级数据时,经常需要进行降采样。例如,我们将tick级是数据重采样为五分钟的数据,然后可以用来回溯测试算法交易测算或者进行技术分析:

1
2
3
tick_resam = tick.resample(rule='5min', label='right').last()
tick_resam['Mid'].plot(figsize=(15,12),
title='Five-minute bar data for EUR/USD exchange rate')

output_59_1.png

小结

本文简要介绍了金融时间序列,并使用pandas进行了初步的分析和数据可视化。接下来我们还会进行更加深入的分析。

参考文献

  1. https://en.wikipedia.org/wiki/Reuters_Instrument_Code
  2. http://handbook.reuters.com/
张da统帅 wechat
扫码订阅我的微信公众号