Python金融分析(十一):算法交易

前言

以前接触过一点量化的东西,经常听人说起“回测”一类高大上的词语,总是充满着快活的空气。闻名不如见面,在这里介绍一下如何用Python进行算法交易的回溯测试。前两年量化投资火的时候,“算法交易”、“程序化交易”、“量化分析”、“高频交易”之类的词满天飞,所谓算法交易也就是采用根据算法计算的结果执行交易,而不受认为的干预。用于“算法交易”的算法五花八门,我们会简单介绍一些具有代表性的算法以及如何实现向量化回溯测试(vectorized backtesting)。

简单滑动平均

炒股的大概都知道“技术面”和“基本面”的含义,从目前实际的情况来看,“技术面”的交易策略更容易通过算法的形式固定下来,而“基本面”由于涉及到多源异构数据的融合加以人的主观判断,如果要变成算法可能有更高的门槛。

简单滑动平均(Simple Moving Averages, SMA)作为一种技术面分析手段,可以称得上是技术交易策略的老古董了,我们首先以它作为例子进行回测。

导入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import math
import numpy as np
import pandas as pd
import datetime as dt
import numpy.random as npr
import matplotlib as mpl
import matplotlib.pyplot as plt
import cufflinks as cf
import plotly.graph_objs as go
import plotly.offline as py_offline
import pandas_datareader.data as pddata

%matplotlib inline

plt.style.use('ggplot')
mpl.rcParams['figure.figsize']= [15, 9]
cf.set_config_file(offline=True, world_readable=True,theme='pearl')
py_offline.init_notebook_mode(connected=True)

我们选择了苹果、IBM、微软、亚马逊等公司的股票,标普500的指数和ETF,以及欧元、黄金等ETF作为投资品种,为方便起见,仅使用收盘价格进行回测。

1
2
3
4
5
6
7
symbols = ['AAPL', 'IBM', 'MSFT', 'AMZN', 'SPY', '^GSPC', '^VIX', 'EUR', '^XAU', 'GDX', 'GLD']
data = pddata.DataReader(name=symbols, data_source='yahoo',
start='2010-01-01', end='2019-06-10',
retry_count=3, pause=0.1,
session=None, access_key=None)
data = data['Adj Close']
data.head()
Symbols AAPL AMZN EUR GDX GLD IBM MSFT SPY ^GSPC ^VIX ^XAU
Date
2009-12-31 26.372231 134.520004 15.600000 43.496857 107.309998 98.137283 24.152580 92.561058 1115.099976 21.680000 168.250000
2010-01-04 26.782711 133.899994 15.790000 44.908779 109.800003 99.299347 24.525019 94.130867 1132.989990 20.040001 174.020004
2010-01-05 26.829010 134.690002 16.120001 45.341774 109.699997 98.099792 24.532942 94.380074 1136.520020 19.350000 176.020004
2010-01-06 26.402260 132.250000 16.299999 46.443077 111.510002 97.462532 24.382378 94.446495 1137.140015 19.160000 180.759995
2010-01-07 26.353460 130.000000 16.049999 46.217175 110.820000 97.125160 24.128809 94.845207 1141.689941 19.059999 179.210007

交易策略

之前曾经介绍过,SMA也叫双MA,其基本思路就是价格围绕价值上下波动,通过短期滑动平均、长期滑动平均两者的关系可以确定不同的买入卖出策略,例如:

  • 短期SMA超过长期SMA时,加仓;
  • 反之,减仓。

我们以苹果股票为例:

1
2
3
4
5
6
7
8
9
10
11
sma_s = 42 # 2-month-long
sma_l = 252 # 1-year-long

stock_aapl = data[['AAPL']]
stock_aapl['SMA_S'] = stock_aapl['AAPL'].rolling(sma_s).mean()
stock_aapl['SMA_L'] = stock_aapl['AAPL'].rolling(sma_l).mean()
stock_aapl.dropna(inplace=True)
stock_aapl['Position'] = np.where(stock_aapl['SMA_S'] > stock_aapl['SMA_L'], 1, -1)
fig = stock_aapl.figure(secondary_y="Position", secondary_y_title="Position")
fig.iplot()
py_offline.plot(fig,output_type='div', include_plotlyjs='cdn')

这部分的结果在前面介绍金融数据时间序列的时候已经提过,但是我们还没有测试这种交易策略的收益情况(例如我们可以将SMA策略与单纯持有的策略进行比较)。

向量化回测

网上有许多成熟的回测框架,例如zipline等,其中向量化回测框架简单,适用于简单的快速回测。我们的向量化回测可以分为以下几步:

  1. 计算股票的对数收益,通过与仓位相乘(+1/-1),得到交易的对数收益;
  2. 将交易的对数收益累加,然后进行指数运算就得到了期间的实际收益。

需要注意的是,如果我们的向量化回测做了许多简化,例如没有考虑交易成本(固定费用,买卖差价,贷款成本等),这对于仅在多年内导致少数交易的交易策略而言,这可能是合理的。 此外,还假设所有交易均以Apple股票的当日收盘价格进行。 更现实的回溯测试方法则会考虑这些和其他(市场微观结构)元素。

1
2
3
4
stock_aapl['Returns'] = np.log(stock_aapl['AAPL'] / stock_aapl['AAPL'].shift(1))
stock_aapl['Strategy'] = stock_aapl['Position'].shift(1) * stock_aapl['Returns']
stock_aapl.round(4).head()
np.exp(stock_aapl[['Returns', 'Strategy']].sum())
Symbols
Returns     4.754465
Strategy    2.988914
dtype: float64
1
2
3
# Calculates the annualized volatility for the strategy
# and the benchmark investment
stock_aapl[['Returns', 'Strategy']].std()*252**0.5
Symbols
Returns     0.258811
Strategy    0.258944
dtype: float64

容易发现,SMA的一个特点是其波动性与基准表现是相同的,同时也非常遗憾,SMA的策略在2011年1月1日至2019年6月10日期间,其业绩表现略逊于基准业绩(即长期持有),那么SMA究竟输在了什么地方呢?

从下面的图可以清楚地看到,一直到2019年以前,SMA的表现都好于基准,但是在19年的剧烈波动导致前期积攒的收益荡然无存,这说明SMA策略在市场大幅剧烈震荡时存在反应迟缓的问题。

1
2
3
ax = stock_aapl[['Returns', 'Strategy']].cumsum().apply(np.exp).plot()
stock_aapl['Position'].plot(ax=ax, secondary_y='Position', style='--')
ax.get_legend().set_bbox_to_anchor((0.25, 0.85));

output_17_0

策略优化

从前面对SMA的介绍可知,SMA可以调整的关键参数是滑动窗口的长度,直观来看,这个长度应该跟市场结构、经济周期乃至人的心理等都有关,因此选择合适的参数有助于取得更加稳健和可观的收益。由于需要优化的参数并不多,我们可以通过暴力搜索的方式遍历参数空间,对策略进行优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from itertools import product
sma_s = range(20, 61, 1)
sma_l = range(180, 281, 1)
results = pd.DataFrame()
for sma1, sma2 in product(sma_s, sma_l):
data = pd.DataFrame(stock_aapl)
data.dropna(inplace=True)
data['Returns'] = np.log(data['AAPL'] / data['AAPL'].shift(1))
data['SMA_S'] = data['AAPL'].rolling(sma1).mean()
data['SMA_L'] = data['AAPL'].rolling(sma2).mean()
data.dropna(inplace=True)
data['Position'] = np.where(data['SMA_S'] > data['SMA_L'], 1, -1)
data['Strategy'] = data['Position'].shift(1) * data['Returns']
data.dropna(inplace=True)
perf = np.exp(data[['Returns', 'Strategy']].sum())
results = results.append(pd.DataFrame(
{
'SMA_S': sma1, 'SMA_L': sma2, 'MARKET': perf['Returns'],
'STRATEGY': perf['Strategy'],
'OUT': perf['Strategy'] - perf['Returns']}, index=[0]), ignore_index=True)

results.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4141 entries, 0 to 4140
Data columns (total 5 columns):
SMA_S       4141 non-null int64
SMA_L       4141 non-null int64
MARKET      4141 non-null float64
STRATEGY    4141 non-null float64
OUT         4141 non-null float64
dtypes: float64(3), int64(2)
memory usage: 161.8 KB
1
results.sort_values('OUT', ascending=False).head()
SMA_S SMA_L MARKET STRATEGY OUT
514 25 189 3.939960 4.212079 0.272119
513 25 188 3.876050 4.143755 0.267705
613 26 187 3.854207 4.105526 0.251319
516 25 191 4.107928 4.209369 0.101440
515 25 190 4.035535 4.135188 0.099653
1
2
3
import seaborn as sns
results_show = results.pivot('SMA_S', 'SMA_L', 'OUT')
ax = sns.heatmap(results_show)

output_22_0

从图上我们可以观察到一下现象:

  • 大部分情况下,策略没有跑赢市场;
  • 左上部分的参数表现相对较好;
  • 图中存在有多个极大值点。

这个我的感觉就是:策略是不靠谱的,运气好可能跑赢大盘,很难发现运气的规律。另一方面,其实如果我们换一只股票、换一个时间段,可能结果就完全不一样了,如果参数对训练集非常敏感的话,意味着可能存在过拟合的问题,这也限制了策略的泛化性能。而且,股市并非是一个平稳的序列,即使有模型也可能是时变的,这一切都意味着算法交易并不是件简单的事。

随机游走假设

多年来,经济学家、统计学家和金融民工们一直对开发和测试股票价格行为模型感兴趣。从这项研究中发展而来的一个重要模型是随机游走理论。该理论对描述和预测股票价格行为的许多其他方法提出了严重质疑。

所谓随机游走假设(Random walk hypothesis, RWH),就是指基于过去的表现,无法预测股票价格未来的变化情况。我们知道有效市场假说分为强有效市场、半强有效市场和弱有效市场三种,随机游走假设实际上就对应着弱有效市场。

那么随机游走的假设下,我们是不能根据股票的历史情况来推断股票未来价格的,那么通过历史数据寻找交易策略还有什么意义呢? 随机游走假设金融市场的价格随机走动,或者在连续时间内,是一个没有漂移的算术布朗运动。未来任何一点没有漂移的算术布朗运动的期望值等于它今天的值。因此,如果RWH适用,那么在最小二乘意义上,明天价格的最佳预测值是今天的价格。

RWH和EMH已经得到了广泛的实证支持,从这个意义上讲,任何算法交易策略都必须通过证明RWH并不绝对适用来证明其价值——显然这也不是件轻而易举的事情。