如何用R做Backtesting
How to Use R for Backtesting
宽谷量化训练营黄金山
November9,2013
本文档主要介绍了使用R做回测,包括了数据的读取,数据的预处理,做图可视化以及一些常用技巧.在阅读本文档之前,读者应该至少阅读过The R Manuals中的An Introduction to R和R Data Import/Export,并且对其中的内容比较熟悉.本文档会尽量包含一些需要注意的技术细节,但也不可能面面俱到.如果遇到问题可以和我们讨论,或是直接利用google搜索一下(尽量使用英文,R的中文用户还没有matlab那样多).此外,Stack Overflow是一个问问题的好去处,凡是和编程有关(不限于R)的问题都可以在上面问.
在开始正题之前,先向大家推荐几款写code的编辑器,正所谓“工欲善其事,必先利其器.”
?RStudio:方便易用,功能齐全,跨平台,目前最好用的IDE,推荐一般用户使用.
?vim-r-plugin:vim和R整合的插件,跨平台,推荐对vim比较熟的用户使用.
?ESS:emacs和R整合的emacs包,功能强大,推荐喜欢emacs的用户使用.
1数据的导入和输出
导入数据的方式主要有以下几种方式:
?文本文件数据,如一般的txt文件和csv文件.
?数据库,SQL Server,MySQL,PostgreSQL.
?网络数据,html表格,XML,json格式的数据.
我们来逐一介绍这些导入数据的函数或者是包.
1.1读取文本数据
读取文本文件是最基本,也是最常用的导入数据的方法.主要是用read.table系列命令,包括read.csv,read.csv2等命令.
1
R Code1.1.1.
stk2007<-read.csv("./2007/stk.csv",colClasses=c(rep("character",2),
rep("numeric",5),rep("NULL",3),
rep("numeric",3)))
?读取时可以指定csv文件每列的类型,"character"表示是字符串,"numeric"表示是数值型,"NULL"表示不读取该列.建议每次读取数据的时候都指定每列的类型,这样会提高读取速度,同时不容易出错.
?如果数据文件中有中文,读取时可能会出现乱码,可以尝试添加参数fileEncoding= "GBK"或是fileEncoding="UTF-8"试试看.
有些文本文件在记录数字的时候可能将长数字没三位用逗号分割,读取这类数据可以先将该列读成"character",然后再做处理;一步到位的办法如下:
R Code1.1.2.
##定义一个新类型https://www.360docs.net/doc/7b2313425.html,ma
setClass("https://www.360docs.net/doc/7b2313425.html,ma")
##定义一个character到https://www.360docs.net/doc/7b2313425.html,ma的转换函数
setAs("character","https://www.360docs.net/doc/7b2313425.html,ma",
function(from)as.numeric(gsub(",","",from))
)
##读取文件,注意指定了类型
com2012<-read.csv("/home/hjs/R_ws/data/comdaily2012.csv",
colClasses=c(rep("character",5),rep("https://www.360docs.net/doc/7b2313425.html,ma",9)))
1.2读取数据库数据
R连接SQL Server可以使用RODBC或者是RJDBC,连接R:
?RODBC(windows only)
–安装RODBC,目前windows支持较好,linux也能装,但比较麻烦.
install.packages("RODBC")
–配置ODBC数据源.控制面板=>管理工具=>数据源(ODBC)=>添加
–R中建立连接查询
2
R Code1.2.1.
library(RODBC)
odbcDataSources()#查看有哪些数据源
#连接数据库
conn<-odbcConnect(dsn="jydb",uid="abc",pwd="abcxxx")
#也可以不设置DSN直接连
conn<-odbcDriverConnect("Driver={SQL Server};Port=1433;
Server=jydb;Uid=xhth;Pwd=zzs2012") ##查询测试
sqlText<-"SELECT*FROM SecuMain"
odbcQuery(conn,sqlText)#查询
#取得查询结果,注意odbcQuery只是查询不会返回结果
queryResults<-sqlGetResults(conn)
queryResults
?RJDBC(windows or linux)
–安装rJava
*windows需要安装JRE或JDK,配置好环境变量,然后安装rJava包.安装了
JDK的用户请注意JAVA_HOME这个环境变量需要以jre结尾,如
##原来JAVA_HOME=C:\Program Files\Java\jdk1.7.0_15,需要改为
JAVA_HOME=C:\Program Files\Java\jdk1.7.0_15\jre
*ubuntu用户可以直接通过如下命令安装
sudo apt-get install r-cran-rjava#安装R包
*opensuse用户,需要修改环境变量,在文件/etc/profile添加
#一般情况默认的是/usr/lib64/jvm/java/需要在后面加jre
export JAVA_HOME=/usr/lib64/jvm/java/jre
并执行命令
sudo R CMD javareconf
–安装RJDBC包,直接安装.
–使用方法
R Code1.2.2.
require(RJDBC)
##driverClass,现在要连的是sqlserver,mysql是"com.mysql.jdbc.Driver"
##classPath微软提供的JDBC driver的位置,需要自己下载调整
3
drv<-JDBC(driverClass="com.microsoft.sqlserver.jdbc.SQLServerDriver",
classPath="/home/hjs/jdbcDriver/sqljdbc4.jar")
##连接聚源数据库
conn<-dbConnect(drv,"jdbc:sqlserver://jydb","abc","abcxxx")
##查询测试
sqlText<-"SELECT*FROM SecuMain"
queryResults<-dbGetQuery(conn,sqlText)
queryREsults
?RPostgreSQL(windows or linux)
–安装RPostgreSQL包,直接安装.
–使用方法
R Code1.2.3.
library(RPostgreSQL)
drv<-dbDriver("PostgreSQL")
con<-dbConnect(drv,host="xhdb",dbname="abc_tmp",user="abc",
password="abcxxx")
df<-dbGetQuery(con,"select*from etf_cr_info")
head(df)
1.3读取网络数据
有些数据可能需要从网上获取,可以考虑使用以下几个包:
?XML包,readHTMLTable函数,读取html文件中的表格.
?RCurl包.参考RCurl包的使用
?rjson包.
这部分内容不是我们关注的重点,需要的时候仔细研究帮助文档和示例.
1.4数据的输出和存储
1.输出到文本文件,一般用write.table系列函数,包括有write.csv,write.csv2等,主要是将
一个data.frame写入一个文本文件.示例:
4
R Code1.4.1.
write.csv(stk,file="stk.csv",https://www.360docs.net/doc/7b2313425.html,s=F,quote=F,
fileEncoding="GBK")
?https://www.360docs.net/doc/7b2313425.html,s一般需要设置成FALSE,否则会多出一列,是每行的名称,默认是行数.
quote一般也需要设置成FALSE,否则字符串默认会加引号.
?fileEncoding写入文件时的编码格式,根据需要设置,默认是UTF-8.
2.输出到'.RData'格式文件.'.RData'格式的文件是R用来存储数据的文件格式.可以将某
个或者某些变量通过函数save保存到'.RData'文件中,下次需要使用的时候再用函数load把文件载入到内存当中.示例:
R Code1.4.2.
##保存当前工作空间下var1和var2到文件vars.RData文件
save(var1,var2,file="vars.RData")
##再次开启R的时候,load一下就可以载入变量var1和var2.
load("vars.RData")
3.保存整个工作空间,用命令save.image保存整个工作空间,事实上退出R的时候会提示
保存当前的工作空间,默认保存当当前的工作目录下.RData文件,也可以指定其它名字的文件.仍然用load载入工作空间.
2数据的预处理
一般而言,我们面对的股票或者期货数据都是时间序列数据.对于R的时间日期处理请查看链接R Date Time Class,此外需要注意的是时区问题,参考链接R Time Zones.为了避免时区导致的错误转换,在进行时间日期操作的时候,可以一开始将时区同一设置成"GMT"或是"UTC",标准时间:
Sys.setenv(TZ="UTC")#set TZ="UTC"
尽管R里面处理日期和时间的函数和相关的包已经很多,但是处理时间日期仍然不是一件容易的事.一种方式是不考虑日期和时间,只要将数据对齐,用整数下标代表日期时间即可,这样使用R中的数据结构data.frame或是matrix就可以处理(推荐使用matrix,随机访问快).另一种选择就是使用xts包.
5
2.1XTS包
xts的含义是Extensible Time Series.该包提供了较统一的时间日期处理方式,非常适合处理股票或者期货的时间序列.本质上讲一个xts对象就是一个matrix加上一个时间日期的index,因此R中的常用数据操作函数全都支持,并且xts提供一套比较完善的函数,用来支持xts对象的下标访问,不同周期时间序列的转换,以及做图等功能.可以参考xts包的帮助文档CRAN:xts.pdf.由于xts的高效易用,很多其它的包都依赖于xts包.
在使用xts对象的时候,可能有一点不是很方便,就是不同时间戳的数据不能直接做运算,解决方法是用coredata取得xts中的matrix数据进行操作.
在处理多只股票数据时,往往会遇到某只或多只停牌,为了方便研究我们往往要对齐数据的时间,时间对齐可能会产生缺失值NA,一般有两种方法来处理缺失值,
?删除含有缺失值的记录,使用na.omit函数处理.
?用与缺失值相近近的值替代,使用na.locf函数处理.
2.2技术指标:TTR包
?常用的技术指标的计算函数我们可以直接通过载入TTR包得到.该包的帮助文档CRAN:TTR.pdf包含了这些技术指标的定义和基本含义,以及一些链接(帮助我们理解这些指标的用法).
?TTR包中还有几个用Fortran和C写的函数:runSD,runMean,runMax,runSum,runCov 等,利用它们我们可以很方便的编写自己需要的技术指标,并保持运行效率.
2.3产生信号的函数
信号可以是多种多样的,这里只给出一些常用的,更多的还是需要自己创造.
R Code2.3.1.
###比较两列数据,可以是vector或是单变量xts,按指定关系返回一个逻辑值向量sigCompar<-function(datax,datay,relationship=c("gt",
"lt","eq","gte","lte")){
relationship<-match.arg(relationship)
opr<-switch(relationship,
gt=,`>`=">",
lt=,`<`="<",
eq=,`==`=,`=`="==",
gte=,gteq=,ge=,`>=`=">=",
6
lte=,lteq=,le=,`<=`="<=")
sig<-do.call(opr,list(datax,datay))
return(sig)
}
###判断两列数据是否出现交叉信号,返回一个逻辑值向量
sigCross<-function(datax,datay,relationship=c("gt",
"lt","eq","gte","lte")){
require(xts)
ret_sig=FALSE
temp.sigcompar<-sigCompar(datax,datay,relationship)
if(inherits(temp.sigcompar,c("xts","zoo")))
ret_sig<-suppressWarnings(ret_sig|diff.xts(temp.sigcompar)==1) else
ret_sig=suppressWarnings(ret_sig|diff.xts(as.numeric(temp.sigcompar))==1) ret_sig[is.na(ret_sig)]<-FALSE
return(ret_sig)
}
#####test the function##############################
datax<-rnorm(10)
datay<-rnorm(10)
sigCompar(datax,datay,relationship="gt")
##判断datax是否从上向下穿过datay
sigCross(datax,datay,relationship="lt")
##做图验证
plot(datax,type="l")
lines(datay,type="l",color="red")
需要注意的是:
?有很多信号是对技术指标进行比较得到,此时可以尽量采用向量化的运算,并结合R自带或是xts提供的lag,diff等函数直接生成逻辑向量.
?不要用到未来的数据来生成现在的信号(原则上任何未来的信息你都不可能在产生交易信号的时候知道).
?此外还要注意xts和R自带的一些函数的结果不同,如diff,xts中的diff会尽量保证结果的长度与原数据的长度对齐.
7
2.4其它推荐的包
在处理金融时间序列数据的时候,下面几个包可能会让你事半功倍:
?data.table包.
?plyr包.
?reshape2包.
?quantmod包.
2.5示例:快速将tick data转换成bar data
下面展示两个个例子如何将tick data快速的转换成对应的bar data,考虑期货CU0603的tick data文件CU0603.txt如下
date time最新成交量
2005122908:59:004195082
2005122909:00:024195078
2005122909:00:02419502
2005122909:00:04419502
2005122909:00:06419804
2005122909:00:06419702
....
首先我们利用xts包和plyr包来实现这个功能:
R Code2.5.1.
library(xts)
library(plyr)
Sys.setenv(TZ="UTC")
cu.test<-read.table("CU0603.txt",header=T,fileEncoding="gbk")
##删除非交易时间数据,实际上还应该删除上午的中场休息时间的数据.
cu.test<-subset(cu.test,as.character(time)>"09:00:00"&
as.character(time)<="11:30:00"&
as.character(time)>"13:00:00"&
as.character(time)<="15:00:00")
##处理时间
datatime<-with(cu.test,paste(date,time))
datetime<-as.POSIXct(datatime,format="%Y%m%d%H:%M:%S")
##xts中函数align.time,60表示60秒,一分钟的bar,如果是5分钟,300
8
datetime<-align.time(datetime-1,60)
##重组数据
CU0603<-data.frame(datetime=datetime,
price=cu.test$最新,
volume=cu.test$成交量)
##转换数据
ddply(CU0603,.(datetime),summarise,
open=price[1],
close=price[length(price)],
high=max(price,na.rm=T),
low=min(price,na.rm=T),
volume=sum(volume))
接着我们再利用强大的data.table包来做同样的事:
R Code2.5.2.
library(data.table)
Sys.setenv(TZ="UTC")
cu.test<-read.table("CU0603.txt",header=T,
colClasses=c(rep("character",2),
rep("numeric",2)),
fileEncoding="gbk")
CU0603<-data.table(idate=as.IDate(cu.test$date,format="%Y%m%d"),
itime=as.ITime(cu.test$time,format="%H:%M:%S"),
last=cu.test$最新,
volume=cu.test$成交量)
tn<-60#60表示一分钟
CU0603[itime>as.ITime("09:00:00")&
itime<=as.ITime("11:30:00")&
itime>as.ITime("13:00:00")&
itime<=as.ITime("15:00:00"),
list(open=最新[1],
close=最新[length(最新)],
high=max(最新),
low=min(最新),
volume=sum(成交量)),
by=list(idate,itime=as.ITime(((time-1)%/%tn+1)*tn),id)]
9
3回测流程
一般看研究报告或是自己观察有了想法,确定了自己的买入卖出规则,就要利用历史数据进行回测,验证你的规则是否能够获得稳定持续的收益,同时你也可能发现问题或是改进的方法.由于不同的买卖规则,最优的回测过程可能不同,有一些简单的可以直接通过向量化计算快速完成回测,但是一些比较复杂的买卖规则必须通过循环来做.这里我们推荐的方式是通过对历史回测期的交易日期进行循环,同时如果是日内的策略需要对每一个bar或是tick 做循环.这么做的好处是逻辑清楚,和真实交易过程一致,不易犯常见错误(利用未来的数据产生现在的信号).但是如果回测期比较长或是频率比较快的策略,回测程序运行的时间就会比较长.
无论是日间的策略,还是日内策略,我们最关心的实际上是每日的PnL(profit and loss,也就是损益).这里每日PnL指的是相对于前一天的损益,如果是日间的策略,需要考虑昨日持仓到今日净值的变化以及当日交易的损益,如果是日内策略就是当日交易的损益.基于每日的损益,我们一般会计算日收益率,整个回测期内的每日的日收益率是评估一个策略好坏的基础,可以根据日收益率计算相关的评估指标,这些下节再讲.
为了计算每日的损益,对于日间策略,需要记录每日的交易情况,同时也要记录当日结束时的每种资产的仓位用于计算第二日的损益;对于日间的策略只需要记录交易情况.推荐使用list或是matrix记录这些信息.这些交易记录,仓位记录也会是评估策略表现的重要指标.
我们用下标i表示第i中资产,下标j表示第j个交易日,上标k表示第k笔交易,则第j 日的P nL j的计算公式如下:
P nL j=
∑
i
P nL i,j,(1)
P nL i,j=posqty i,j?1×(clprc i,j?clprc i,j?1)
+
∑
k
{txnqty k i,j×(clprc i,j?txnprc k i,j)?txncost k i,j},(2) posqty i,j=posqty i,j?1+
∑
k
txnqty k i,j(3)其中,posqty i,j?1是第i项资产在j?1天结束时的仓位,clprc i,j是第i项资产在j天的收盘
价,txnqty k
i,j 和txnprc k
i,j
分别是第i项资产在j天的第k笔交易的交易量和交易价格,其中
txnqty k i,j是带有正负号的,正的表示买入(做多),负的表示卖出(做空).txncost k i,j表示该次交易的交易成本.
交易成本分为固定的交易佣金,税费,以及冲击成本,滑差.进行真实交易的时候,在买入和卖出信号触发后,一般是发限价单,等待交易所撮合,并不是看到什么价格就能用这个价格成交,执行价格和预期价格之间的滑差很可能会将你的盈利全部磨平.一般对于股票策略如
10
果交易量不是特别大,可以认为买入时佣金是5bps(万分之一),冲击成本是5bps,卖出的时候印花税是10bps,冲击成本是5bps.股指期货的交易佣金是每次交易(买入或买出)0.3bps,冲击成本和滑差会根据交易频率和成交量大小变化,可以尝试将冲击成本调大一点,如调成2bps,看收益率是否会明显变化,如果变化很剧烈,就要重新考虑策略或是做进一步的研究.关于回测的时间,一般不能太短,否则回测的结果可能依赖回测期内的特殊行情,因此回测期应该包含不同行情,用以测试策略在不同情况下的表现.但也不能时间过长,时间过长可能找不到一个好的策略,此外市场很可能在这么长的回测期中出现了本质上的变化,开发出的策略只能适应旧的市场环境,在新的市场环境中表现不是特别好,但由于回测期中旧市场环境占的时间很大,策略总体的表现非常不错,但在实盘的时候就不是那么好.
4评估指标介绍
有了日损益序列序列,再除以初始资金,就可以得到日收益序列(注意我们没有使用复利来计算收益),根据日收益率序列,我们主要考虑年化收益率,年化Sharpe Ratio,最大回撤.如果是股票的多空策略,需要记录每天交易股票的个数,如果个数少的天数很多说明该策略并不是特别理想.期货的日内的高频策略,还需要考虑日平均最大持仓,日平均换手率等指标.事实上,看一张累计收益率(PnL)的图就可以大概知道该策略的好坏.
1.年化收益率:计算公式mean (daily.return )×scale ,其中mean (daily.Return )是daily.return 的均值,scale 是一年有多少个交易日,我们取定中国市场一年有scale =250作为标准.
2.年化Sharpe Ratio:Sharpe Ratio 的计算公式是:mean (daily.return )/sd (daily.return ),其中sd (daily.return )表示daily.return 的标准差.年化的Sharpe Ratio 是Sharpe Ratio 乘以一个因子,计算公式是Sharpe.Ratio ×√scale ,其中scale =250表示一年的交易日天数.
3.最大回撤:回撤(百分比)的计算方法是,记CR i 是到第i 天的累计收益率(累计PnL),第k 天的回撤是
Drawdown k =CR k ?max 1≤i ≤k CR i (4)
最大回撤即回撤的最小值.
我们写了一个R 包,可以将日收益率序列或是期货日内交易记录转换成标准的回测报告,请参考KGOstdReport 包的使用说明文档.
11
5数据可视化
一张图往往胜过千言万语.做图往往可以更直观的帮助我们查找统计规律,开发信号,评估策略的稳定性.R作为开源的统计计算图形系统,做图功能还是非常完善强大的,可用的函数和包也有很多,本节主要介绍一下我们常用的函数或者包.
对于一般的统计图形,如密度图,箱线图,直方图,QQ图等,R都有相应的函数实现,关于R基本绘图函数,推荐几本书
?R Graphics Second Edition
?R Graphics Cookbook
?现代统计图形,谢益辉写的,貌似还没出版,但网上有一些版本可以下得到.
在回测中我们最有可能需要绘制展示一些金融时间序列相关的图,正如之前提到的,xts 包提供了函数plot.xts可以对xts对象做图,满足一般性的需求,不过功能有限,但是其它依赖xts的包扩展了对xts对象的做图功能,它们是:
?PerformanceAnalytics包,该包中以chart开头的一系列函数可以绘制不同需要的时间序列图.最常用的是charts.TimeSeries和chart.Bar.
?quantmod包,该包可以绘制各种K线图,配合TTR包还能添加各种技术指标图,功能全面而强大,可以很方便的添加交易信号.比较常用的函数是chartSeries.参考帮助文档CRAN:quantmod.pdf.
最后向大家推荐神奇的ggplot2包.ggplot2开创了一种独特的做图语法,它将绘图视为一种映射,即从数据映射到图形元素空间.例如将不同的数值映射到不同的色彩或透明度.该绘图包的特点在于并不去定义具体的图形(如直方图,散点图).而是定义各种底层组件(如线条,方块)来合成复杂的图形,这使它能以非常简洁的函数构建各类图形.在用它绘图时,只需要用"+"号叠加相应的绘图对象即可,绘图对象可以保存,调整,非常方便.此外ggplot2绘制出来的图也更精美.大家可以参考:
?ggplot2:Elegant Graphics for Data Analysis该书已经被翻译成中文版.
?ggplot2的官方文档链接:ggplot2Doc
?以及ggplot2的帮助文档CRAN:ggplot2.pdf
12
6如何Debug
写程序出错是难免的,如何高效的调试程序,修改错误是我们必须面对的一个问题.R作为解释性的语言,对于小代码量的程序很容易调试,只需要逐行运行便可找到错误并修正,但当代码累积到一定量的时候,这么做就显得有点低效.有些人说R做Debug很困难,估计这话说的时候估计有点早,现在R做Debug已经非常容易:
?最常见的方式是利用print函数,把你想看到的变量在特定的地方输出,查看是否有错.
此外R中的函数traceback可以帮你找到出错的函数,再利用debug和browse可以找到函数内部哪里出错.这个方法仍然不是特别有效率.
?幸运的是最新版本的Rstudio0.9.8提供了Debug的工具,详细请参考Debugging with Rstudio.
?ESS也同样提供了专门用来Debug的工具,参考Debugging with ESS.
7常用的技巧
本节总结了一些常用的技巧,希望有帮助:
1.开启R的即时编译功能JIT.如果R程序中有大量的循环语句,R会比较慢,可以开启R
的即时编译功能,提升R的运行速度(3x-5x),在运行一段程序前,运行如下语句
R Code7.0.3.
require(compiler)
enableJIT(3)
2.并行计算.R在版本2.14的时候加入了Parallel包,该包提供了一些可供并行计算的函
数例如:mclapply,mcparallel,clusterApply,可以利用Cluster或是多个CPU进行并行计算,提高运行效率.结合R Revolution提供的包doParallel,我们能够很方便编写并行的程序,提高我们CPU的使用率(目前我们的个人台式机是四核心四线程,如果把主要的循环放到不同CPU里去跑,可以大概提升4x的速度).此外plyr中的有些函数就支持并行计算.
R Code7.0.4.
library(doParallel)
###use snow-like functionality,windows or Linux
cl<-makeCluster(4)
13
registerDoParallel(cl)
foreach(i=1:4)%dopar%sqrt(i)
###stop the snow cluster when you are done using it
stopCluster(cl)
###use multicore-like functionality,Linux only
require(doParallel)
registerDoParallel(cores=4)
foreach(i=1:4)%dopar%sqrt(i)
更多的详细信息请参考Getting Started with doParallel and foreach.pdf
3.矩阵运算,参考链接R矩阵运算
4.正则表达式,参考链接Regular Expressions as used in R.
5.减少显示循环的使用,因为R做显示循环有点慢,尽量使用apply系列的函数:apply,
lapply,mapply,sapply.
6.处理大数据.最简单的方法是换64位的操作系统,然后加内存条.使用数据库也是不错
的选择,但维护数据库也会耗费较多时间.推荐使用R中的ff包和bigmemory包.
7.Stack Overflow上总结的What is the Most Useful R Trick,非常值得看一看.
14