现在,Notebook 新增功能,支持运行回测(包括股票策略回测、期货策略回测及混合策略回测)及参数优化。
也就是说,Notebook 中不仅可以方便地运行回测,还可以非常简便调整参数并获取自定义的回测报告。
首先,点击工具栏中最右侧的“生成回测代码模版”,选择“股票策略”,我们将看到如下的默认股票策略回测模版。
%%rqalpha_plus -s 20160301 -e 20160901 --account stock 100000 -fq 1d -p -bm 000001.XSHG
# 上述命令参数可以通过运行 %%rqalpha_plus -h 查看到
def init(context):
# 策略初始化运行
logger.info('init')
context.counter = 0
def before_trading(context):
# 每日开盘前运行
pass
def handle_bar(context, bar_dict):
# 每个 bar 数据运行
context.counter += 1
if context.counter == 1:
order_shares('000001.XSHE', 100)
def after_trading(context):
# 每日收盘后运行
pass
回测模版中的命令参数
我们可以通过
%rqalpha_plus -h
来查看所有的命令参数。
其中比较常用的有:
-s 回测起始日期
-e 回测结束日期
--account stock 100000 股票账户初始资金 100000
-fq 回测评率
-p 打印回测结果
-bm 市场基准
%rqalpha_plus -h
获取回测报告
运行完回测后,报告会自动存储到 report 变量中。可以直接通过 report 变量获取当次回测的结果。
另外 notebook 的 mod 的输出会自动存储在 results 变量中。
results.keys()
report.keys()
例如我们想看一下回测的 summary,这将会非常方便:
report.summary
回测功能
我们通过一个简单的“麦克•贝利 2-2-2 选股法则”来熟悉一下新的回测功能。
麦克•贝利简介
麦克•贝利(Michael Berry),亚利桑那大学数量分析博士,财务研究专家,对美国股市具有深刻的研究经验,同时也是价值投资的拥护者。麦克•贝利在其发表的研究成果阐述了自己的价值投资理念,并发展出一种简单的选股标 准,称为 2-2-2 法则。
麦克•贝利 2-2-2 选股法则的通用版本
A. 股票预期市盈率低于市场平均预期市盈率的“2”分之一
B. 公司预期盈利成长率大于市场平均预估盈利成长率的“2”分之一
C. 股票的市净率小于“2”(此处我们选择“股票的市净率小于市场平均预估市净率的‘2’分之一”)
具体回测代码如下
%%rqalpha_plus -s 20160101 -e 20170101 --account stock 100000 -fq 1d -p -bm 000001.XSHG
# 上述命令参数可以通过运行 %%rqalpha_plus -h 查看到
import numpy as np
import pandas as pd
import math
from pandas import Series
import statsmodels.api as sm
import talib as tb
# 在这个方法中编写任何的初始化逻辑。context对象将会在你的算法策略的任何方法之间做传递。
def init(context):
# 在context中保存全局变量
context.num = 20
# 设置全局计数器,用于调仓次数的计数
context.count = 0
#context.stocks = []
# 实时打印日志
#logger.info("RunInfo: {}".format(context.run_info))
if context.count % 4 == 0:
scheduler.run_monthly(rebalance,1)
context.count +=1
# before_trading此函数会在每天策略交易开始前被调用,当天只会被调用一次
def before_trading(context):
all_df = get_fundamentals(
query(
fundamentals.eod_derivative_indicator.pb_ratio, # 市净率
fundamentals.eod_derivative_indicator.pe_ratio, # 市盈率
fundamentals.financial_indicator.earnings_per_share, # 每股收益 EPS
fundamentals.eod_derivative_indicator.market_cap # 总市值
#盈利成长率=每股业绩=每股收益EPS
)
)
# dropna 返回一个仅含非空数据和索引值的 Series
all_df = all_df.dropna(axis = 1,how = 'any')
# 转置
all_df = all_df.T
# shape 用于读取矩阵的长度
n = max(np.shape(all_df.values))
#print(sum(all_df.values))
# 计算(1/2 * 市场平均市净率)
context.pb = (sum(all_df.values)/(2*n))[0]
# 计算(1/2 * 市场平均市盈率)
context.pe = (sum(all_df.values)/(2*n))[1]
# 计算(1/2 * 市场平均每股收益)
context.eps = (sum(all_df.values)/(2*n))[2]
#print('pb='+str(context.pb))
#print('pe='+str(context.pe))
#print('eps='+str(context.eps))
all_df = all_df[all_df['pb_ratio']<2]
all_df = all_df[all_df['pe_ratio']<context.pe]
all_df = all_df[all_df['earnings_per_share']> context.eps]
all_df = all_df.sort_values(by = 'market_cap',ascending = True)
#logger.info('all_df'+str(all_df))
all_df = all_df.head(context.num)
context.all_df = all_df
context.stocks = context.all_df.index.values
#logger.info("选择好的股票列表为:" + str(context.stocks))
# 你选择的证券的数据更新将会触发此段逻辑,例如日或分钟历史数据切片或者是实时数据切片更新
def rebalance(context, bar_dict):
# 开始编写你的主要的算法逻辑
# bar_dict[order_book_id] 可以拿到某个证券的bar信息
# context.portfolio 可以拿到现在的投资组合信息
# 使用order_shares(id_or_ins, amount)方法进行落单
# TODO: 开始编写你的算法吧!
average_weight = 0
if len(context.stocks) != 0:
average_weight = 0.999 / len(context.stocks)
#开始进行调仓
#先清仓持仓的股票不在新的列表中
for stock in context.portfolio.positions: #从已有持仓的股票中选取
if stock not in context.stocks: #如果不在新的股票列表里
order_target_percent(stock,0) #清仓
if average_weight != 0:
for stock in context.stocks:
order_target_percent(stock,average_weight)
# after_trading函数会在每天交易结束后被调用,当天只会被调用一次
def after_trading(context):
pass
获取自定义的回测报告
report.summary
results["sys_analyser"]["trades"][:5]
我们来看一下部分交易记录
report.trades[:5]
from hmmlearn.hmm import GaussianHMM
import numpy as np
from matplotlib import cm, pyplot as plt
import matplotlib.dates as dates
import pandas as pd
import datetime
import warnings
warnings.filterwarnings('ignore')
回测功能再认识
接下来是一个更为复杂的策略——HMM在股票市场中的应用
我们假设隐藏状态数量是6,即假设股市的状态有6种,虽然我们并不知道每种状态到底是什么,但是通过后面的图我们可以看出那种状态下市场是上涨的,哪种是震荡的,哪种是下跌的。可观测的特征状态我们选择了3个指标进行标示,进行预测的时候假设假设所有的特征向量的状态服从高斯分布,这样就可以使用 hmmlearn 这个包中的 GaussianHMM 进行预测了。下面我会逐步解释。
首先导入必要的包:
from hmmlearn.hmm import GaussianHMM
import numpy as np
from matplotlib import cm, pyplot as plt
import matplotlib.dates as dates
import pandas as pd
import datetime
测试时间从2005年1月1日到2015年12月31日,拿到每日沪深300的各种交易数据。
beginDate = '2005-01-01'
endDate = '2015-12-31'
n = 6 #6个隐藏状态
data = get_price('CSI300.INDX',start_date=beginDate, end_date=endDate,frequency='1d')
data[0:9]
拿到每日成交量和收盘价的数据。
volume = data['TotalVolumeTraded']
close = data['ClosingPx']
计算每日最高最低价格的对数差值,作为特征状态的一个指标。
logDel = np.log(np.array(data['HighPx'])) - np.log(np.array(data['LowPx']))
logDel
计算每5日的指数对数收益差,作为特征状态的一个指标。
logRet_1 = np.array(np.diff(np.log(close)))#这个作为后面计算收益使用
logRet_5 = np.log(np.array(close[5:])) - np.log(np.array(close[:-5]))
logRet_5
计算每5日的指数成交量的对数差,作为特征状态的一个指标。
logVol_5 = np.log(np.array(volume[5:])) - np.log(np.array(volume[:-5]))
logVol_5
由于计算中出现了以5天为单位的计算,所以要调整特征指标的长度。
logDel = logDel[5:]
logRet_1 = logRet_1[4:]
close = close[5:]
Date = pd.to_datetime(data.index[5:])
把我们的特征状态合并在一起。
A = np.column_stack([logDel,logRet_5,logVol_5])
A
下面运用 hmmlearn 这个包中的 GaussianHMM 进行预测。
model = GaussianHMM(n_components= n, covariance_type="full", n_iter=2000).fit(A)
hidden_states = model.predict(A)
hidden_states
关于 covariance_type 的参数有下面四种:
spherical:是指在每个马尔可夫隐含状态下,可观察态向量的所有特性分量使用相同的方差值。对应协方差矩阵的非对角为0,对角值相等,即球面特性。这是最简单的高斯分布PDF。
diag:是指在每个马尔可夫隐含状态下,可观察态向量使用对角协方差矩阵。对应协方差矩阵非对角为0,对角值不相等。diag是hmmlearn里面的默认类型。
full:是指在每个马尔可夫隐含状态下,可观察态向量使用完全协方差矩阵。对应的协方差矩阵里面的元素都是不为零。
tied:是指所有的马尔可夫隐含状态使用相同的完全协方差矩阵。
这四种PDF类型里面,spherical, diag和full代表三种不同的高斯分布概率密度函数,而tied则可以看作是GaussianHMM和GMMHMM的特有实现。其中,full是最强大的,但是需要足够多的数据来做合理的参数估计;spherical是最简单的,通常用在数据不足或者硬件平台性能有限的情况之下;而diag则是这两者一个折中。在使用的时候,需要根据可观察态向量不同特性的相关性来选择合适的类型。
转自知乎用户Aubrey Li
我们把每个预测的状态用不同颜色标注在指数曲线上看一下结果。
import warnings
warnings.filterwarnings('ignore')
plt.figure(figsize=(25, 18))
for i in range(model.n_components):
pos = (hidden_states==i)
plt.plot_date(Date[pos],close[pos],'o',label='hidden state %d'%i,lw=2)
plt.legend(loc="left")
res = pd.DataFrame({'Date':Date,'logRet_1':logRet_1,'state':hidden_states}).set_index('Date')
plt.figure(figsize=(25, 18))
for i in range(model.n_components):
pos = (hidden_states==i)
pos = np.append(0,pos[:-1])#第二天进行买入操作
df = res.logRet_1
res['state_ret%s'%i] = df.multiply(pos)
plt.plot_date(Date,np.exp(res['state_ret%s'%i].cumsum()),'-',label='hidden state %d'%i)
plt.legend(loc="left")
可以看到,隐藏状态1是一个明显的大牛市阶段,隐藏状态0是一个缓慢上涨的阶段(可能对应反弹),隐藏状态3和5可以分别对应震荡下跌的大幅下跌。其他的两个隐藏状态并不是很明确。由于股指期货可以做空,我们可以进行如下操作:当处于状态0和1时第二天做多,当处于状态3和5第二天做空,其余状态则不持有。
long = (hidden_states==0) + (hidden_states == 1) #做多
short = (hidden_states==3) + (hidden_states == 5) #做空
long = np.append(0,long[:-1]) #第二天才能操作
short = np.append(0,short[:-1]) #第二天才能操作
收益曲线图如下:
res['ret'] = df.multiply(long) - df.multiply(short)
plt.plot_date(Date,np.exp(res['ret'].cumsum()),'r-')
可以看到效果还是很不错的。但事实上该结果是有些问题的。真实操作时,我们并没有未来的信息来训练模型。不过可以考虑用历史数据进行训练,再对之后的数据进行预测。
from rqalpha_plus.api import *
from rqalpha_plus import run_func
from hmmlearn.hmm import GaussianHMM
import numpy as np
from matplotlib import cm, pyplot as plt
import matplotlib.dates as dates
import pandas as pd
import datetime
def init(context):
# 策略初始化运行
context.now
context.stock = '000300.XSHG'
context.A = 1
def before_trading(context):
yesterday = (context.now-datetime.timedelta(days = 1)).strftime('%Y-%m-%d')
ago = (context.now-datetime.timedelta(days = 100)).strftime('%Y-%m-%d')
data = get_price('000300.XSHG',start_date = ago,end_date = yesterday)
volume = data['volume']
close = data['close']
logDel = np.log(np.array(data['high'])) - np.log(np.array(data['low']))
logRet_1 = np.array(np.diff(np.log(close)))#这个作为后面计算收益使用
logRet_5 = np.log(np.array(close[5:])) - np.log(np.array(close[:-5]))
logVol_5 = np.log(np.array(volume[5:])) - np.log(np.array(volume[:-5]))
logDel = logDel[5:]
logRet_1 = logRet_1[4:]
close = close[5:]
Date = pd.to_datetime(data.index[5:])
A = np.column_stack([logDel,logRet_5,logVol_5])
context.A = A
def handle_bar(context, bar_dict):
# 每个 bar 数据运行
hidden_states = model.predict(context.A)
if hidden_states[-1] == 2:
order_target_percent(context.stock,1)
else:
order_target_percent(context.stock,0)
def after_trading(context):
# 每日收盘后运行
pass
config = {
"base": {
"start_date": "2016-01-01",
"end_date": "2016-06-30",
"benchmark": "000300.XSHG",
"accounts": {
'stock': 100000,
}
},
"extra": {
"log_level": "verbose",
},
"mod": {
"sys_analyser": {
"enabled": True,
"plot": True
}
}
}
# 您可以指定您要传递的参数
run_func(init=init, before_trading=before_trading, handle_bar=handle_bar, config=config)
from rqdatac import *
get_price('000300.XSHG',start_date = '2017-07-05',end_date = '2017-07-05')
from rqdatac import *
参数调优
现在,notebook 不仅可以运行回测,更可以在其中进行参数调优,免去在策略页面中多次修改参数、多次回测的重复劳动。
下面,我们通过一个小例子来一窥究竟。
跨期合约价差研究
首先,我们发现 RU1605 和RU1606 的价差始终都处于比较平稳的状态,并且带有剧烈的波动,于是以历史两倍标准差作为动态开平仓线,进行跨期套利。
那么这个两倍标准差究竟是不是最优值呢?我们运用 notebook 的参数优化功能,以步长 0.1 考察标准差从 2 倍到 3 倍的情况。
我们把需要调整的参数放入 tasks,回测的主逻辑不变,并发运行回测,最终得到不同参数值下的一系列结果。
start_date='20151121'
end_date='20160430'
RU1605=get_price('RU1605',start_date=start_date,end_date=end_date,fields='close',frequency='1m') #当月
RU1606=get_price('RU1606',start_date=start_date,end_date=end_date,fields='close',frequency='1m') # 下月
spread=RU1605.values-RU1606.values
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
plt.figure(figsize=[40,12])
plt.plot(spread)
plt.legend(['spread'])
mean=np.ones(spread.shape)*np.mean(spread)
data=pd.DataFrame([spread,mean],index=['spread','mean']).T
data[['spread','mean']].plot(figsize=[40,12])