如何用R做Backtesting

如何用R做Backtesting
如何用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

相关主题
相关文档
最新文档